Compare commits

..

1 Commits

Author SHA1 Message Date
henrygd
47360c5bf1 update to json/v2 - pocketbase collection errors :( 2025-08-20 20:13:01 -04:00
132 changed files with 4884 additions and 7145 deletions

View File

@@ -93,9 +93,7 @@ jobs:
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Login to Docker Hub - name: Login to Docker Hub
env: if: github.event_name != 'pull_request'
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ matrix.username || secrets[matrix.username_secret] }} username: ${{ matrix.username || secrets[matrix.username_secret] }}
@@ -110,6 +108,6 @@ jobs:
context: "${{ matrix.context }}" context: "${{ matrix.context }}"
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }} platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }} push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}

View File

@@ -51,4 +51,3 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
IS_FORK: ${{ github.repository_owner != 'henrygd' }}

View File

@@ -38,25 +38,12 @@ builds:
- mips64 - mips64
- riscv64 - riscv64
- mipsle - mipsle
- mips
- ppc64le - ppc64le
gomips:
- hardfloat
- softfloat
ignore: ignore:
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
- goos: openbsd - goos: openbsd
goarch: arm goarch: arm
- goos: linux
goarch: mips64
gomips: softfloat
- goos: linux
goarch: mipsle
gomips: hardfloat
- goos: linux
goarch: mips
gomips: hardfloat
- goos: windows - goos: windows
goarch: arm goarch: arm
- goos: darwin - goos: darwin
@@ -67,7 +54,7 @@ builds:
archives: archives:
- id: beszel-agent - id: beszel-agent
formats: [tar.gz] formats: [tar.gz]
ids: builds:
- beszel-agent - beszel-agent
name_template: >- name_template: >-
{{ .Binary }}_ {{ .Binary }}_
@@ -79,7 +66,7 @@ archives:
- id: beszel - id: beszel
formats: [tar.gz] formats: [tar.gz]
ids: builds:
- beszel - beszel
name_template: >- name_template: >-
{{ .Binary }}_ {{ .Binary }}_
@@ -98,7 +85,7 @@ nfpms:
API access. API access.
maintainer: henrygd <hank@henrygd.me> maintainer: henrygd <hank@henrygd.me>
section: net section: net
ids: builds:
- beszel-agent - beszel-agent
formats: formats:
- deb - deb
@@ -135,7 +122,6 @@ scoops:
homepage: "https://beszel.dev" homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform." description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
# # Needs choco installed, so doesn't build on linux / default gh workflow :( # # Needs choco installed, so doesn't build on linux / default gh workflow :(
# chocolateys: # chocolateys:
@@ -169,7 +155,7 @@ brews:
homepage: "https://beszel.dev" homepage: "https://beszel.dev"
description: "Agent for Beszel, a lightweight server monitoring platform." description: "Agent for Beszel, a lightweight server monitoring platform."
license: MIT license: MIT
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}" skip_upload: auto
extra_install: | extra_install: |
(bin/"beszel-agent-launcher").write <<~EOS (bin/"beszel-agent-launcher").write <<~EOS
#!/bin/bash #!/bin/bash
@@ -201,7 +187,7 @@ winget:
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}" release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
publisher_support_url: "https://github.com/henrygd/beszel/issues" publisher_support_url: "https://github.com/henrygd/beszel/issues"
short_description: "Agent for Beszel, a lightweight server monitoring platform." short_description: "Agent for Beszel, a lightweight server monitoring platform."
skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}" skip_upload: auto
description: | description: |
Beszel is a lightweight server monitoring platform that includes Docker Beszel is a lightweight server monitoring platform that includes Docker
statistics, historical data, and alert functions. It has a friendly web statistics, historical data, and alert functions. It has a friendly web

View File

@@ -17,7 +17,7 @@ clean:
lint: lint:
golangci-lint run golangci-lint run
test: export GOEXPERIMENT=synctest test: export GOEXPERIMENT=synctest,jsonv2
test: test:
go test -tags=testing ./... go test -tags=testing ./...
@@ -70,6 +70,7 @@ dev-server: generate-locales
fi fi
dev-hub: export ENV=dev dev-hub: export ENV=dev
dev-hub: export GOEXPERIMENT=jsonv2
dev-hub: dev-hub:
mkdir -p ./site/dist && touch ./site/dist/index.html mkdir -p ./site/dist && touch ./site/dist/index.html
@if command -v entr >/dev/null 2>&1; then \ @if command -v entr >/dev/null 2>&1; then \
@@ -78,6 +79,7 @@ dev-hub:
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \ cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
fi fi
dev-agent: export GOEXPERIMENT=jsonv2
dev-agent: dev-agent:
@if command -v entr >/dev/null 2>&1; then \ @if command -v entr >/dev/null 2>&1; then \
find ./cmd/agent/*.go ./internal/agent/*.go | entr -r go run beszel/cmd/agent; \ find ./cmd/agent/*.go ./internal/agent/*.go | entr -r go run beszel/cmd/agent; \

View File

@@ -4,12 +4,12 @@ import (
"beszel" "beszel"
"beszel/internal/agent" "beszel/internal/agent"
"beszel/internal/agent/health" "beszel/internal/agent/health"
"flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -17,24 +17,43 @@ import (
type cmdOptions struct { type cmdOptions struct {
key string // key is the public key(s) for SSH authentication. key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on. 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. // parse parses the command line flags and populates the config struct.
// It returns true if a subcommand was handled and the program should exit. // It returns true if a subcommand was handled and the program should exit.
func (opts *cmdOptions) parse() bool { 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 := "" subcommand := ""
if len(os.Args) > 1 { if len(os.Args) > 1 {
subcommand = os.Args[1] subcommand = os.Args[1]
} }
// Subcommands that don't require any pflag parsing
switch subcommand { switch subcommand {
case "-v", "version": case "-v", "version":
fmt.Println(beszel.AppName+"-agent", beszel.Version) fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true return true
case "help":
flag.Usage()
return true
case "update":
agent.Update()
return true
case "health": case "health":
err := health.Check() err := health.Check()
if err != nil { if err != nil {
@@ -44,57 +63,7 @@ func (opts *cmdOptions) parse() bool {
return true return true
} }
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true flag.Parse()
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 return false
} }

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ COPY internal ./internal
# Build # Build
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent RUN CGO_ENABLED=0 GOGC=75 GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
RUN rm -rf /tmp/* RUN rm -rf /tmp/*

View File

@@ -10,12 +10,12 @@ COPY internal ./internal
# Build # Build
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent RUN CGO_ENABLED=0 GOGC=75 GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
# -------------------------- # --------------------------
# Final image: GPU-enabled agent with nvidia-smi # Final image: GPU-enabled agent with nvidia-smi
# -------------------------- # --------------------------
FROM nvidia/cuda:12.2.2-base-ubuntu22.04 FROM nvidia/cuda:12.9.1-base-ubuntu22.04
COPY --from=builder /agent /agent COPY --from=builder /agent /agent
ENTRYPOINT ["/agent"] ENTRYPOINT ["/agent"]

View File

@@ -22,7 +22,7 @@ RUN update-ca-certificates
# Build # Build
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub RUN CGO_ENABLED=0 GOEXPERIMENT=jsonv2 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
# ? ------------------------- # ? -------------------------
FROM scratch FROM scratch

View File

@@ -7,18 +7,18 @@ replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr
require ( require (
github.com/blang/semver v3.5.1+incompatible github.com/blang/semver v3.5.1+incompatible
github.com/distatus/battery v0.11.0
github.com/fxamacker/cbor/v2 v2.9.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lxzan/gws v1.8.9 github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.8.17 github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3 github.com/pocketbase/pocketbase v0.29.2
github.com/shirou/gopsutil/v4 v4.25.6 github.com/rhysd/go-github-selfupdate v1.2.3
github.com/shirou/gopsutil/v4 v4.25.7
github.com/spf13/cast v1.9.2 github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.0 github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.41.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@@ -40,9 +40,12 @@ require (
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -50,8 +53,10 @@ require (
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.7 // indirect github.com/spf13/pflag v1.0.7 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
github.com/ulikunitz/xz v0.5.13 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/image v0.30.0 // indirect golang.org/x/image v0.30.0 // indirect
@@ -60,8 +65,6 @@ require (
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.66.3 // indirect modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

View File

@@ -13,8 +13,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=
github.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
@@ -27,6 +25,7 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
@@ -49,26 +48,39 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d h1:vFzYZc8yji+9DmNRhpEbs8VBK4CgV/DPfGzeVJSSp/8= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM= github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y= github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -79,8 +91,11 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.8.8 h1:F/oyoatWK5cbHPPgkjRZrA0262TP7KWuUQz9KskRtR8= github.com/nicholas-fedor/shoutrrr v0.8.8 h1:F/oyoatWK5cbHPPgkjRZrA0262TP7KWuUQz9KskRtR8=
github.com/nicholas-fedor/shoutrrr v0.8.8/go.mod h1:T30Y+eoZFEjDk4HtOItcHQioZSOe3Z6a6aNfSz6jc5c= github.com/nicholas-fedor/shoutrrr v0.8.8/go.mod h1:T30Y+eoZFEjDk4HtOItcHQioZSOe3Z6a6aNfSz6jc5c=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -88,17 +103,19 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.29.3 h1:Mj8o5awsbVJIdIoTuQNhfC2oL/c4aImQ3RyfFZlzFVg= github.com/pocketbase/pocketbase v0.29.2 h1:MghVgLYy/xh9lBwHtteNSYjYOvHKYD+dS9pzUzOP79Q=
github.com/pocketbase/pocketbase v0.29.3/go.mod h1:oGpT67LObxCFK4V2fSL7J9YnPbBnnshOpJ5v3zcneww= github.com/pocketbase/pocketbase v0.29.2/go.mod h1:QZPKtMCWfiDJb0aLhwgj7ZOr6O8tusbui2EhTFAHThU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
@@ -108,12 +125,17 @@ github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.13 h1:ar98gWrjf4H1ev05fYP/o29PDZw9DrI3niHtnEqyuXA=
github.com/ulikunitz/xz v0.5.13/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -121,6 +143,7 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
@@ -130,53 +153,71 @@ golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=

View File

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

View File

@@ -1,53 +0,0 @@
//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

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

View File

@@ -4,7 +4,7 @@ import (
"beszel/internal/entities/container" "beszel/internal/entities/container"
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json/v2"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -29,7 +29,6 @@ type dockerManager struct {
goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly) goodDockerVersion bool // Whether docker version is at least 25.0.0 (one-shot works correctly)
isWindows bool // Whether the Docker Engine API is running on Windows isWindows bool // Whether the Docker Engine API is running on Windows
buf *bytes.Buffer // Buffer to store and read response bodies buf *bytes.Buffer // Buffer to store and read response bodies
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object apiStats *container.ApiStats // Reusable API stats object
} }
@@ -343,17 +342,16 @@ func newDockerManager(a *Agent) *dockerManager {
// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe. // Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.
func (dm *dockerManager) decode(resp *http.Response, d any) error { func (dm *dockerManager) decode(resp *http.Response, d any) error {
if dm.buf == nil { if dm.buf == nil {
// initialize buffer with 256kb starting size // initialize buffer with 128kb starting size
dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256)) dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*128))
dm.decoder = json.NewDecoder(dm.buf)
} }
defer resp.Body.Close() defer resp.Body.Close()
defer dm.buf.Reset() dm.buf.Reset()
_, err := dm.buf.ReadFrom(resp.Body) _, err := dm.buf.ReadFrom(resp.Body)
if err != nil { if err != nil {
return err return err
} }
return dm.decoder.Decode(d) return json.Unmarshal(dm.buf.Bytes(), d)
} }
// Test docker / podman sockets and return if one exists // Test docker / podman sockets and return if one exists

View File

@@ -4,7 +4,7 @@ import (
"beszel/internal/entities/system" "beszel/internal/entities/system"
"bufio" "bufio"
"bytes" "bytes"
"encoding/json" "encoding/json/v2"
"fmt" "fmt"
"os/exec" "os/exec"
"regexp" "regexp"
@@ -50,7 +50,7 @@ type GPUManager struct {
// RocmSmiJson represents the JSON structure of rocm-smi output // RocmSmiJson represents the JSON structure of rocm-smi output
type RocmSmiJson struct { type RocmSmiJson struct {
ID string `json:"GUID"` ID string `json:"GUID"`
Name string `json:"Card series"` Name string `json:"Card Series"`
Temperature string `json:"Temperature (Sensor edge) (C)"` Temperature string `json:"Temperature (Sensor edge) (C)"`
MemoryUsed string `json:"VRAM Total Used Memory (B)"` MemoryUsed string `json:"VRAM Total Used Memory (B)"`
MemoryTotal string `json:"VRAM Total Memory (B)"` MemoryTotal string `json:"VRAM Total Memory (B)"`

View File

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

View File

@@ -4,7 +4,8 @@ import (
"beszel" "beszel"
"beszel/internal/common" "beszel/internal/common"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"encoding/json" "encoding/json/jsontext"
"encoding/json/v2"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -144,7 +145,7 @@ func (a *Agent) writeToSession(w io.Writer, stats *system.CombinedData, hubVersi
if hubVersion.GTE(beszel.MinVersionCbor) { if hubVersion.GTE(beszel.MinVersionCbor) {
return cbor.NewEncoder(w).Encode(stats) return cbor.NewEncoder(w).Encode(stats)
} }
return json.NewEncoder(w).Encode(stats) return json.MarshalEncode(jsontext.NewEncoder(w), stats)
} }
// extractHubVersion extracts the beszel version from SSH client version string. // extractHubVersion extracts the beszel version from SSH client version string.

View File

@@ -5,7 +5,7 @@ import (
"beszel/internal/entities/system" "beszel/internal/entities/system"
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"encoding/json" "encoding/json/v2"
"fmt" "fmt"
"net" "net"
"os" "os"

View File

@@ -2,7 +2,6 @@ package agent
import ( import (
"beszel" "beszel"
"beszel/internal/agent/battery"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"bufio" "bufio"
"fmt" "fmt"
@@ -60,10 +59,10 @@ func (a *Agent) initializeSystemInfo() {
} }
// zfs // zfs
if _, err := getARCSize(); err != nil { if _, err := getARCSize(); err == nil {
slog.Debug("Not monitoring ZFS ARC", "err", err)
} else {
a.zfs = true a.zfs = true
} else {
slog.Debug("Not monitoring ZFS ARC", "err", err)
} }
} }
@@ -71,11 +70,6 @@ func (a *Agent) initializeSystemInfo() {
func (a *Agent) getSystemStats() system.Stats { func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{} systemStats := system.Stats{}
// battery
if battery.HasReadableBattery() {
systemStats.Battery[0], systemStats.Battery[1], _ = battery.GetBatteryStats()
}
// cpu percent // cpu percent
cpuPct, err := cpu.Percent(0, false) cpuPct, err := cpu.Percent(0, false)
if err != nil { if err != nil {
@@ -86,6 +80,7 @@ func (a *Agent) getSystemStats() system.Stats {
// load average // load average
if avgstat, err := load.Avg(); err == nil { if avgstat, err := load.Avg(); err == nil {
// TODO: remove these in future release in favor of load avg array
systemStats.LoadAvg[0] = avgstat.Load1 systemStats.LoadAvg[0] = avgstat.Load1
systemStats.LoadAvg[1] = avgstat.Load5 systemStats.LoadAvg[1] = avgstat.Load5
systemStats.LoadAvg[2] = avgstat.Load15 systemStats.LoadAvg[2] = avgstat.Load15

View File

@@ -1,150 +1,56 @@
package agent package agent
import ( import (
"beszel/internal/ghupdate" "beszel"
"fmt" "fmt"
"log"
"os" "os"
"os/exec"
"strings" "strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
) )
// restarter knows how to restart the beszel-agent service. // Update updates beszel-agent to the latest version
type restarter interface { func Update() {
Restart() error var latest *selfupdate.Release
} var found bool
var err error
type systemdRestarter struct{ cmd string } currentVersion := semver.MustParse(beszel.Version)
fmt.Println("beszel-agent", currentVersion)
func (s *systemdRestarter) Restart() error { fmt.Println("Checking for updates...")
// Only restart if the service is active updater, _ := selfupdate.NewUpdater(selfupdate.Config{
if err := exec.Command(s.cmd, "is-active", "beszel-agent.service").Run(); err != nil { Filters: []string{"beszel-agent"},
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent.service via systemd…")
return exec.Command(s.cmd, "restart", "beszel-agent.service").Run()
}
type openRCRestarter struct{ cmd string }
func (o *openRCRestarter) Restart() error {
if err := exec.Command(o.cmd, "status", "beszel-agent").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via OpenRC…")
return exec.Command(o.cmd, "restart", "beszel-agent").Run()
}
type openWRTRestarter struct{ cmd string }
func (w *openWRTRestarter) Restart() error {
if err := exec.Command(w.cmd, "running", "beszel-agent").Run(); err != nil {
return nil
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via procd…")
return exec.Command(w.cmd, "restart", "beszel-agent").Run()
}
func detectRestarter() restarter {
if path, err := exec.LookPath("systemctl"); err == nil {
return &systemdRestarter{cmd: path}
}
if path, err := exec.LookPath("rc-service"); err == nil {
return &openRCRestarter{cmd: path}
}
if path, err := exec.LookPath("service"); err == nil {
return &openWRTRestarter{cmd: path}
}
return nil
}
// Update checks GitHub for a newer release of beszel-agent, applies it,
// fixes SELinux context if needed, and restarts the service.
func Update(useMirror bool) error {
exePath, _ := os.Executable()
dataDir, err := getDataDir()
if err != nil {
dataDir = os.TempDir()
}
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel-agent",
DataDir: dataDir,
UseMirror: useMirror,
}) })
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil { if err != nil {
log.Fatal(err) fmt.Println("Error checking for updates:", err)
} os.Exit(1)
if !updated {
return nil
} }
// make sure the file is executable if !found {
if err := os.Chmod(exePath, 0755); err != nil { fmt.Println("No updates found")
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set executable permissions: %v", err) os.Exit(0)
}
// set ownership to beszel:beszel if possible
if chownPath, err := exec.LookPath("chown"); err == nil {
if err := exec.Command(chownPath, "beszel:beszel", exePath).Run(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set file ownership: %v", err)
}
} }
// 6) Fix SELinux context if necessary fmt.Println("Latest version:", latest.Version)
if err := handleSELinuxContext(exePath); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: SELinux context handling: %v", err) if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return
} }
// 7) Restart service if running under a recognised init system var binaryPath string
if r := detectRestarter(); r != nil { fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
if err := r.Restart(); err != nil { binaryPath, err = os.Executable()
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.")
}
return nil
}
// handleSELinuxContext restores or applies the correct SELinux label to the binary.
func handleSELinuxContext(path string) error {
out, err := exec.Command("getenforce").Output()
if err != nil { if err != nil {
// SELinux not enabled or getenforce not available fmt.Println("Error getting binary path:", err)
return nil os.Exit(1)
} }
state := strings.TrimSpace(string(out)) err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
if state == "Disabled" { if err != nil {
return nil fmt.Println("Please try rerunning with sudo. Error:", err)
os.Exit(1)
} }
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
ghupdate.ColorPrint(ghupdate.ColorYellow, "SELinux is enabled; applying context…")
var errs []string
// Try persistent context via semanage+restorecon
if semanagePath, err := exec.LookPath("semanage"); err == nil {
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "semanage fcontext failed: "+err.Error())
} else if restoreconPath, err := exec.LookPath("restorecon"); err == nil {
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
errs = append(errs, "restorecon failed: "+err.Error())
}
}
}
// Fallback to temporary context via chcon
if chconPath, err := exec.LookPath("chcon"); err == nil {
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
errs = append(errs, "chcon failed: "+err.Error())
}
}
if len(errs) > 0 {
return fmt.Errorf("SELinux context errors: %s", strings.Join(errs, "; "))
}
return nil
} }

View File

@@ -2,7 +2,7 @@ package alerts
import ( import (
"beszel/internal/entities/system" "beszel/internal/entities/system"
"encoding/json" "encoding/json/v2"
"fmt" "fmt"
"strings" "strings"
"time" "time"

View File

@@ -36,10 +36,8 @@ type Stats struct {
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes] Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes] MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
// TODO: remove other load fields in future release in favor of load avg array // 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 { type GPUData struct {
@@ -83,27 +81,27 @@ const (
) )
type Info struct { type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"` Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"` Cores int `json:"c" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"` Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"` CpuModel string `json:"m" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"` Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"` Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"` MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"` DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"` Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"` AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"` GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"` DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"` Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
// TODO: remove load fields in future release in favor of load avg array // TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
} }
// Final data structure to return to the hub // Final data structure to return to the hub

View File

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

View File

@@ -1,348 +0,0 @@
// Package ghupdate implements a new command to self update the current
// executable with the latest GitHub release. This is based on PocketBase's
// ghupdate package with modifications.
package ghupdate
import (
"beszel"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/blang/semver"
)
// Minimal color functions using ANSI escape codes
const (
colorReset = "\033[0m"
ColorYellow = "\033[33m"
ColorGreen = "\033[32m"
colorCyan = "\033[36m"
colorGray = "\033[90m"
)
func ColorPrint(color, text string) {
fmt.Println(color + text + colorReset)
}
func ColorPrintf(color, format string, args ...interface{}) {
fmt.Printf(color+format+colorReset+"\n", args...)
}
// HttpClient is a base HTTP client interface (usually used for test purposes).
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// Config defines the config options of the ghupdate plugin.
//
// NB! This plugin is considered experimental and its config options may change in the future.
type Config struct {
// Owner specifies the account owner of the repository (default to "pocketbase").
Owner string
// Repo specifies the name of the repository (default to "pocketbase").
Repo string
// ArchiveExecutable specifies the name of the executable file in the release archive
// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
ArchiveExecutable string
// Optional context to use when fetching and downloading the latest release.
Context context.Context
// The HTTP client to use when fetching and downloading the latest release.
// Defaults to `http.DefaultClient`.
HttpClient HttpClient
// 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 := &updater{
currentVersion: beszel.Version,
config: config,
}
return p.update()
}
func (p *updater) update() (updated bool, err error) {
ColorPrint(ColorYellow, "Fetching release information...")
if p.config.DataDir == "" {
p.config.DataDir = os.TempDir()
}
if p.config.Owner == "" {
p.config.Owner = "henrygd"
}
if p.config.Repo == "" {
p.config.Repo = "beszel"
}
if p.config.Context == nil {
p.config.Context = context.Background()
}
if p.config.HttpClient == nil {
p.config.HttpClient = http.DefaultClient
}
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,
apiURL,
)
if err != nil {
return false, err
}
currentVersion := semver.MustParse(strings.TrimPrefix(p.currentVersion, "v"))
newVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, "v"))
if newVersion.LTE(currentVersion) {
ColorPrintf(ColorGreen, "You already have the latest version %s.", p.currentVersion)
return false, nil
}
suffix := archiveSuffix(p.config.ArchiveExecutable, runtime.GOOS, runtime.GOARCH)
asset, err := latest.findAssetBySuffix(suffix)
if err != nil {
return false, err
}
releaseDir := filepath.Join(p.config.DataDir, ".beszel_update")
defer os.RemoveAll(releaseDir)
ColorPrintf(ColorYellow, "Downloading %s...", asset.Name)
// download the release asset
assetPath := filepath.Join(releaseDir, asset.Name)
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, useMirror); err != nil {
return false, err
}
ColorPrintf(ColorYellow, "Extracting %s...", asset.Name)
extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name)
defer os.RemoveAll(extractDir)
// Extract the archive (automatically detects format)
if err := extract(assetPath, extractDir); err != nil {
return false, err
}
ColorPrint(ColorYellow, "Replacing the executable...")
oldExec, err := os.Executable()
if err != nil {
return false, err
}
renamedOldExec := oldExec + ".old"
defer os.Remove(renamedOldExec)
newExec := filepath.Join(extractDir, p.config.ArchiveExecutable)
if _, err := os.Stat(newExec); err != nil {
// try again with an .exe extension
newExec = newExec + ".exe"
if _, fallbackErr := os.Stat(newExec); fallbackErr != nil {
return false, fmt.Errorf("the executable in the extracted path is missing or it is inaccessible: %v, %v", err, fallbackErr)
}
}
// rename the current executable
if err := os.Rename(oldExec, renamedOldExec); err != nil {
return false, fmt.Errorf("failed to rename the current executable: %w", err)
}
tryToRevertExecChanges := func() {
if revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil {
slog.Debug(
"Failed to revert executable",
slog.String("old", renamedOldExec),
slog.String("new", oldExec),
slog.String("error", revertErr.Error()),
)
}
}
// replace with the extracted binary
if err := os.Rename(newExec, oldExec); err != nil {
// If rename fails due to cross-device link, try copying instead
if isCrossDeviceError(err) {
if err := copyFile(newExec, oldExec); err != nil {
tryToRevertExecChanges()
return false, fmt.Errorf("failed replacing the executable: %w", err)
}
} else {
tryToRevertExecChanges()
return false, fmt.Errorf("failed replacing the executable: %w", err)
}
}
ColorPrint(colorGray, "---")
ColorPrint(ColorGreen, "Update completed successfully!")
// print the release notes
if latest.Body != "" {
fmt.Print("\n")
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")
}
return true, nil
}
func fetchLatestRelease(
ctx context.Context,
client HttpClient,
url string,
) (*release, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
rawBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
// http.Client doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return nil, fmt.Errorf(
"(%d) failed to fetch latest releases:\n%s",
res.StatusCode,
string(rawBody),
)
}
result := &release{}
if err := json.Unmarshal(rawBody, result); err != nil {
return nil, err
}
return result, nil
}
func downloadFile(
ctx context.Context,
client HttpClient,
url string,
destPath string,
useMirror bool,
) error {
if useMirror {
url = strings.Replace(url, "github.com", "gh.beszel.dev", 1)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
// http.Client doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return fmt.Errorf("(%d) failed to send download file request", res.StatusCode)
}
// ensure that the dest parent dir(s) exist
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
dest, err := os.Create(destPath)
if err != nil {
return err
}
defer dest.Close()
if _, err := io.Copy(dest, res.Body); err != nil {
return err
}
return nil
}
// isCrossDeviceError checks if the error is due to a cross-device link
func isCrossDeviceError(err error) bool {
return err != nil && (strings.Contains(err.Error(), "cross-device") ||
strings.Contains(err.Error(), "EXDEV"))
}
// copyFile copies a file from src to dst, preserving permissions
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
// Copy the file contents
if _, err := io.Copy(destFile, sourceFile); err != nil {
return err
}
// Preserve the original file permissions
sourceInfo, err := sourceFile.Stat()
if err != nil {
return err
}
return destFile.Chmod(sourceInfo.Mode())
}
func archiveSuffix(binaryName, goos, goarch string) string {
if goos == "windows" {
return fmt.Sprintf("%s_%s_%s.zip", binaryName, goos, goarch)
}
return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch)
}

View File

@@ -1,45 +0,0 @@
package ghupdate
import (
"path/filepath"
"testing"
)
func TestReleaseFindAssetBySuffix(t *testing.T) {
r := release{
Assets: []*releaseAsset{
{Name: "test1.zip", Id: 1},
{Name: "test2.zip", Id: 2},
{Name: "test22.zip", Id: 22},
{Name: "test3.zip", Id: 3},
},
}
asset, err := r.findAssetBySuffix("2.zip")
if err != nil {
t.Fatalf("Expected nil, got err: %v", err)
}
if asset.Id != 2 {
t.Fatalf("Expected asset with id %d, got %v", 2, asset)
}
}
func TestExtractFailure(t *testing.T) {
testDir := t.TempDir()
// Test with missing zip file
missingZipPath := filepath.Join(testDir, "missing_test.zip")
extractedPath := filepath.Join(testDir, "zip_extract")
if err := extract(missingZipPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing zip file")
}
// Test with missing tar.gz file
missingTarPath := filepath.Join(testDir, "missing_test.tar.gz")
if err := extract(missingTarPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing tar.gz file")
}
}

View File

@@ -1,36 +0,0 @@
package ghupdate
import (
"errors"
"strings"
)
type releaseAsset struct {
Name string `json:"name"`
DownloadUrl string `json:"browser_download_url"`
Id int `json:"id"`
Size int `json:"size"`
}
type release struct {
Name string `json:"name"`
Tag string `json:"tag_name"`
Published string `json:"published_at"`
Url string `json:"html_url"`
Body string `json:"body"`
Assets []*releaseAsset `json:"assets"`
Id int `json:"id"`
}
// findAssetBySuffix returns the first available asset containing the specified suffix.
func (r *release) findAssetBySuffix(suffix string) (*releaseAsset, error) {
if suffix != "" {
for _, asset := range r.Assets {
if strings.HasSuffix(asset.Name, suffix) {
return asset, nil
}
}
}
return nil, errors.New("missing asset containing " + suffix)
}

View File

@@ -9,7 +9,7 @@ import (
"bytes" "bytes"
"crypto/ed25519" "crypto/ed25519"
"encoding/json" "encoding/json/v2"
"encoding/pem" "encoding/pem"
"io" "io"
"net/http" "net/http"

View File

@@ -5,7 +5,8 @@ import (
"beszel/internal/entities/system" "beszel/internal/entities/system"
"beszel/internal/hub/ws" "beszel/internal/hub/ws"
"context" "context"
"encoding/json" "encoding/json/jsontext"
"encoding/json/v2"
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
@@ -275,7 +276,7 @@ func (sys *System) fetchDataViaSSH() (*system.CombinedData, error) {
if sys.agentVersion.GTE(beszel.MinVersionCbor) { if sys.agentVersion.GTE(beszel.MinVersionCbor) {
err = cbor.NewDecoder(stdout).Decode(sys.data) err = cbor.NewDecoder(stdout).Decode(sys.data)
} else { } else {
err = json.NewDecoder(stdout).Decode(sys.data) err = json.UnmarshalDecode(jsontext.NewDecoder(stdout), sys.data)
} }
if err != nil { if err != nil {

View File

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

View File

@@ -1,85 +1,57 @@
package hub package hub
import ( import (
"beszel/internal/ghupdate" "beszel"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// Update updates beszel to the latest version // Update updates beszel to the latest version
func Update(cmd *cobra.Command, _ []string) { func Update(_ *cobra.Command, _ []string) {
dataDir := os.TempDir() var latest *selfupdate.Release
var found bool
// set dataDir to ./beszel_data if it exists var err error
if _, err := os.Stat("./beszel_data"); err == nil { currentVersion := semver.MustParse(beszel.Version)
dataDir = "./beszel_data" fmt.Println("beszel", currentVersion)
} fmt.Println("Checking for updates...")
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
// Check if china-mirrors flag is set Filters: []string{"beszel_"},
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel",
DataDir: dataDir,
UseMirror: useMirror,
}) })
latest, found, err = updater.DetectLatest("henrygd/beszel")
if err != nil { if err != nil {
log.Fatal(err) fmt.Println("Error checking for updates:", err)
os.Exit(1)
} }
if !updated {
if !found {
fmt.Println("No updates found")
os.Exit(0)
}
fmt.Println("Latest version:", latest.Version)
if latest.Version.LTE(currentVersion) {
fmt.Println("You are up to date")
return return
} }
// make sure the file is executable var binaryPath string
exePath, err := os.Executable() fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
if err == nil { binaryPath, err = os.Executable()
if err := os.Chmod(exePath, 0755); err != nil { if err != nil {
fmt.Printf("Warning: failed to set executable permissions: %v\n", err) fmt.Println("Error getting binary path:", err)
} os.Exit(1)
} }
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
// Try to restart the service if it's running if err != nil {
restartService() fmt.Println("Please try rerunning with sudo. Error:", err)
} os.Exit(1)
}
// restartService attempts to restart the beszel service fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
func restartService() {
// Check if we're running as a service by looking for systemd
if _, err := exec.LookPath("systemctl"); err == nil {
// Check if beszel service exists and is active
cmd := exec.Command("systemctl", "is-active", "beszel.service")
if err := cmd.Run(); err == nil {
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("systemctl", "restart", "beszel.service")
if err := restartCmd.Run(); err != nil {
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 {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
}
// Check for OpenRC (Alpine Linux)
if _, err := exec.LookPath("rc-service"); err == nil {
cmd := exec.Command("rc-service", "beszel", "status")
if err := cmd.Run(); err == nil {
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("rc-service", "beszel", "restart")
if err := restartCmd.Run(); err != nil {
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 {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
}
ghupdate.ColorPrint(ghupdate.ColorYellow, "Service restart not attempted. If running as a service, restart manually.")
} }

View File

@@ -4,7 +4,7 @@ package records
import ( import (
"beszel/internal/entities/container" "beszel/internal/entities/container"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"encoding/json" "encoding/json/v2"
"fmt" "fmt"
"log" "log"
"math" "math"
@@ -172,8 +172,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
tempStats = system.Stats{} tempStats = system.Stats{}
sum := &sumStats sum := &sumStats
stats := &tempStats stats := &tempStats
// necessary because uint8 is not big enough for the sum
batterySum := 0
count := float64(len(records)) count := float64(len(records))
tempCount := float64(0) tempCount := float64(0)
@@ -210,11 +208,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.LoadAvg[2] += stats.LoadAvg[2] sum.LoadAvg[2] += stats.LoadAvg[2]
sum.Bandwidth[0] += stats.Bandwidth[0] sum.Bandwidth[0] += stats.Bandwidth[0]
sum.Bandwidth[1] += stats.Bandwidth[1] sum.Bandwidth[1] += stats.Bandwidth[1]
batterySum += int(stats.Battery[0])
sum.Battery[1] = stats.Battery[1]
// Set peak values // Set peak values
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu) 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.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv) sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs) sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
@@ -295,7 +290,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count) sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count) sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count) sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
sum.Battery[0] = uint8(batterySum / int(count))
// Average temperatures // Average temperatures
if sum.Temperatures != nil && tempCount > 0 { if sum.Temperatures != nil && tempCount > 0 {
for key := range sum.Temperatures { for key := range sum.Temperatures {

View File

@@ -1,15 +1,12 @@
package tests package tests
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"maps" "maps"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
@@ -251,29 +248,30 @@ func (scenario *ApiScenario) test(t testing.TB) {
} }
} else { } else {
// normalize json response format // normalize json response format
buffer := new(bytes.Buffer) /* buffer := new(bytes.Buffer)
err := json.Compact(buffer, recorder.Body.Bytes()) err := json.Compact(buffer, recorder.Body.Bytes())
var normalizedBody string var normalizedBody string
if err != nil { if err != nil {
// not a json... // not a json...
normalizedBody = recorder.Body.String() normalizedBody = recorder.Body.String()
} else { } else {
normalizedBody = buffer.String() normalizedBody = buffer.String()
} }
for _, item := range scenario.ExpectedContent { for _, item := range scenario.ExpectedContent {
if !strings.Contains(normalizedBody, item) { if !strings.Contains(normalizedBody, item) {
t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody) t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody)
break break
} }
} }
for _, item := range scenario.NotExpectedContent { for _, item := range scenario.NotExpectedContent {
if strings.Contains(normalizedBody, item) { if strings.Contains(normalizedBody, item) {
t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody) t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody)
break break
} }
} }
*/
} }
remainingEvents := maps.Clone(testApp.EventCalls) remainingEvents := maps.Clone(testApp.EventCalls)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{ {
"name": "beszel", "name": "beszel",
"private": true, "private": true,
"version": "0.12.6", "version": "0.12.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite",
"build": "lingui extract --overwrite && lingui compile && vite build", "build": "lingui extract --overwrite && lingui compile && vite build",
"preview": "vite preview", "preview": "vite preview",
"sync": "lingui extract --overwrite && lingui compile", "sync": "lingui extract --overwrite && lingui compile",
@@ -13,27 +13,26 @@
"dependencies": { "dependencies": {
"@henrygd/queue": "^1.0.7", "@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2", "@henrygd/semaphore": "^0.0.2",
"@lingui/detect-locale": "^5.4.1", "@lingui/detect-locale": "^5.3.3",
"@lingui/macro": "^5.4.1", "@lingui/macro": "^5.3.3",
"@lingui/react": "^5.4.1", "@lingui/react": "^5.3.3",
"@nanostores/react": "^0.7.3", "@nanostores/react": "^0.7.3",
"@nanostores/router": "^0.11.0", "@nanostores/router": "^0.11.0",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-direction": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -41,26 +40,28 @@
"lucide-react": "^0.452.0", "lucide-react": "^0.452.0",
"nanostores": "^0.11.4", "nanostores": "^0.11.4",
"pocketbase": "^0.26.2", "pocketbase": "^0.26.2",
"react": "^19.1.1", "react": "^18.3.1",
"react-dom": "^19.1.1", "react-dom": "^18.3.1",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"valibot": "^0.42.1" "valibot": "^0.42.1"
}, },
"devDependencies": { "devDependencies": {
"@lingui/cli": "^5.4.1", "@lingui/cli": "^5.3.3",
"@lingui/swc-plugin": "^5.6.1", "@lingui/swc-plugin": "^5.5.2",
"@lingui/vite-plugin": "^5.4.1", "@lingui/vite-plugin": "^5.3.3",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/vite": "^4.1.12", "@types/bun": "^1.2.19",
"@types/bun": "^1.2.20", "@types/react": "^18.3.23",
"@types/react": "^19.1.11", "@types/react-dom": "^18.3.7",
"@types/react-dom": "^19.1.7", "@vitejs/plugin-react-swc": "^3.11.0",
"@vitejs/plugin-react-swc": "^4.0.1", "autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.12", "postcss": "^8.5.6",
"tw-animate-css": "^1.3.7", "tailwindcss": "^3.4.17",
"typescript": "^5.9.2", "tailwindcss-rtl": "^0.9.0",
"vite": "^7.1.3" "typescript": "^5.8.3",
"vite": "^6.3.5"
}, },
"overrides": { "overrides": {
"@nanostores/router": { "@nanostores/router": {

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -13,15 +13,13 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { $publicKey } from "@/lib/stores" import { $publicKey, pb } from "@/lib/stores"
import { cn, generateToken, tokenMap, useLocalStorage } from "@/lib/utils" import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils"
import { pb, isReadOnlyUser } from "@/lib/api"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react" import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react" import { memo, useEffect, useRef, useState } from "react"
import { $router, basePath, Link, navigate } from "./router" import { $router, basePath, Link, navigate } from "./router"
import { SystemRecord } from "@/types" import { SystemRecord } from "@/types"
import { SystemStatus } from "@/lib/enums"
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons" import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
import { InputCopy } from "./ui/input-copy" import { InputCopy } from "./ui/input-copy"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
@@ -107,7 +105,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
try { try {
setOpen(false) setOpen(false)
if (system) { if (system) {
await pb.collection("systems").update(system.id, { ...data, status: SystemStatus.Pending }) await pb.collection("systems").update(system.id, { ...data, status: "pending" })
} else { } else {
const createdSystem = await pb.collection("systems").create(data) const createdSystem = await pb.collection("systems").create(data)
await pb.collection("fingerprints").create({ await pb.collection("fingerprints").create({
@@ -133,7 +131,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
> >
<Tabs defaultValue={tab} onValueChange={setTab}> <Tabs defaultValue={tab} onValueChange={setTab}>
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-2 max-w-100 truncate pr-8"> <DialogTitle className="mb-2">
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>} {system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
</DialogTitle> </DialogTitle>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
@@ -167,7 +165,9 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
<Trans> <Trans>
Copy the installation command for the agent below, or register agents automatically with a{" "} Copy the installation command for the agent below, or register agents automatically with a{" "}
<Link <Link
onClick={() => setOpen(false)} onClick={() => {
setOpen(false)
}}
href={getPagePath($router, "settings", { name: "tokens" })} href={getPagePath($router, "settings", { name: "tokens" })}
className="link" className="link"
> >
@@ -274,7 +274,7 @@ interface CopyButtonProps {
text: string text: string
onClick: () => void onClick: () => void
dropdownItems: DropdownItem[] dropdownItems: DropdownItem[]
icon?: React.ReactElement<any> icon?: React.ReactElement
} }
const CopyButton = memo((props: CopyButtonProps) => { const CopyButton = memo((props: CopyButtonProps) => {

View File

@@ -2,8 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"
import { AlertsHistoryRecord } from "@/types" import { AlertsHistoryRecord } from "@/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils" import { alertInfo, formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
@@ -16,9 +15,7 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
<Trans>System</Trans> <Trans>System</Trans>
</Button> </Button>
), ),
cell: ({ row }) => ( cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
<div className="ps-2 max-w-60 truncate">{row.original.expand?.system?.name || row.original.system}</div>
),
filterFn: (row, _, filterValue) => { filterFn: (row, _, filterValue) => {
const display = row.original.expand?.system?.name || row.original.system || "" const display = row.original.expand?.system?.name || row.original.system || ""
return display.toLowerCase().includes(filterValue.toLowerCase()) return display.toLowerCase().includes(filterValue.toLowerCase())

View File

@@ -2,22 +2,23 @@ import { t } from "@lingui/core/macro"
import { memo, useMemo, useState } from "react" import { memo, useMemo, useState } from "react"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $alerts } from "@/lib/stores" import { $alerts } from "@/lib/stores"
import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog"
import { BellIcon } from "lucide-react" import { BellIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { SystemRecord } from "@/types" import { SystemRecord } from "@/types"
import { AlertDialogContent } from "./alerts-sheet" import { AlertDialogContent } from "./alerts-dialog"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
export default memo(function AlertsButton({ system }: { system: SystemRecord }) { export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
const [opened, setOpened] = useState(false) const [opened, setOpened] = useState(false)
const alerts = useStore($alerts) const alerts = useStore($alerts)
const hasSystemAlert = alerts[system.id]?.size > 0 const hasSystemAlert = alerts[system.id]?.size > 0
return useMemo( return useMemo(
() => ( () => (
<Sheet> <Dialog>
<SheetTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}> <Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
<BellIcon <BellIcon
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", { className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
@@ -25,12 +26,32 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
})} })}
/> />
</Button> </Button>
</SheetTrigger> </DialogTrigger>
<SheetContent className="max-h-full overflow-auto w-145 !max-w-full p-4 sm:p-6"> <DialogContent className="max-h-full sm:max-h-[95svh] overflow-auto max-w-[37rem]">
{opened && <AlertDialogContent system={system} />} {opened && <AlertDialogContent system={system} />}
</SheetContent> </DialogContent>
</Sheet> </Dialog>
), ),
[opened, hasSystemAlert] [opened, hasSystemAlert]
) )
// return useMemo(
// () => (
// <Sheet>
// <SheetTrigger asChild>
// <Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
// <BellIcon
// className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
// "fill-primary": hasAlert,
// })}
// />
// </Button>
// </SheetTrigger>
// <SheetContent className="max-h-full overflow-auto w-[35em] p-4 sm:p-5">
// {opened && <AlertDialogContent system={system} />}
// </SheetContent>
// </Sheet>
// ),
// [opened, hasAlert]
// )
}) })

View File

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

View File

@@ -7,7 +7,7 @@ import { useMemo } from "react"
export type DataPoint = { export type DataPoint = {
label: string label: string
dataKey: (data: SystemStatsRecord) => number | undefined dataKey: (data: SystemStatsRecord) => number | undefined
color: number | string color: string
opacity: number opacity: number
} }
@@ -18,7 +18,6 @@ export default function AreaChartDefault({
tickFormatter, tickFormatter,
contentFormatter, contentFormatter,
dataPoints, dataPoints,
domain,
}: // logRender = false, }: // logRender = false,
{ {
chartData: ChartData chartData: ChartData
@@ -27,7 +26,6 @@ export default function AreaChartDefault({
tickFormatter: (value: number, index: number) => string tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
dataPoints?: DataPoint[] dataPoints?: DataPoint[]
domain?: [number, number]
// logRender?: boolean // logRender?: boolean
}) { }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
@@ -53,7 +51,7 @@ export default function AreaChartDefault({
orientation={chartData.orientation} orientation={chartData.orientation}
className="tracking-tighter" className="tracking-tighter"
width={yAxisWidth} width={yAxisWidth}
domain={domain ?? [0, max ?? "auto"]} domain={[0, max ?? "auto"]}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))} tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
@@ -70,7 +68,7 @@ export default function AreaChartDefault({
} }
/> />
{dataPoints?.map((dataPoint, i) => { {dataPoints?.map((dataPoint, i) => {
const color = `var(--chart-${dataPoint.color})` const color = `hsl(var(--chart-${dataPoint.color}))`
return ( return (
<Area <Area
key={i} key={i}

View File

@@ -69,9 +69,9 @@ export default memo(function DiskChart({
dataKey={dataKey} dataKey={dataKey}
name={t`Disk Usage`} name={t`Disk Usage`}
type="monotoneX" type="monotoneX"
fill="var(--chart-4)" fill="hsl(var(--chart-4))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="var(--chart-4)" stroke="hsl(var(--chart-4))"
// animationDuration={1200} // animationDuration={1200}
isAnimationActive={false} isAnimationActive={false}
/> />

View File

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

View File

@@ -58,9 +58,9 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
dataKey="stats.su" dataKey="stats.su"
name={t`Used`} name={t`Used`}
type="monotoneX" type="monotoneX"
fill="var(--chart-2)" fill="hsl(var(--chart-2))"
fillOpacity={0.4} fillOpacity={0.4}
stroke="var(--chart-2)" stroke="hsl(var(--chart-2))"
isAnimationActive={false} isAnimationActive={false}
/> />
</AreaChart> </AreaChart>

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react" import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
import { $authenticated } from "@/lib/stores" import { $authenticated, pb } from "@/lib/stores"
import * as v from "valibot" import * as v from "valibot"
import { toast } from "../ui/use-toast" import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@@ -13,7 +13,6 @@ import { useCallback, useEffect, useState } from "react"
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase" import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { $router, Link, prependBasePath } from "../router" import { $router, Link, prependBasePath } from "../router"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { pb } from "@/lib/api"
const honeypot = v.literal("") const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`)) const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
@@ -289,7 +288,7 @@ export function UserAuthForm({
// }} // }}
/> />
)} )}
<span className="translate-y-px">{provider.displayName}</span> <span className="translate-y-[1px]">{provider.displayName}</span>
</button> </button>
))} ))}
</div> </div>
@@ -300,7 +299,7 @@ export function UserAuthForm({
<DialogTrigger asChild> <DialogTrigger asChild>
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}> <button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
<img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" /> <img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
<span className="translate-y-px">GitHub</span> <span className="translate-y-[1px]">GitHub</span>
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent style={{ maxWidth: 440, width: "90%" }}> <DialogContent style={{ maxWidth: 440, width: "90%" }}>

View File

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

View File

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

View File

@@ -1,21 +1,56 @@
import { t } from "@lingui/core/macro" import { Trans } from "@lingui/react/macro";
import { MoonStarIcon, SunIcon } from "lucide-react" import { t } from "@lingui/core/macro";
import { LaptopIcon, MoonStarIcon, SunIcon } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { useTheme } from "@/components/theme-provider" import { useTheme } from "@/components/theme-provider"
import { cn } from "@/lib/utils"
export function ModeToggle() { export function ModeToggle() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const options = [
{
theme: "light",
Icon: SunIcon,
label: <Trans comment="Light theme">Light</Trans>,
},
{
theme: "dark",
Icon: MoonStarIcon,
label: <Trans comment="Dark theme">Dark</Trans>,
},
{
theme: "system",
Icon: LaptopIcon,
label: <Trans comment="System theme">System</Trans>,
},
]
return ( return (
<Button <DropdownMenu>
variant={"ghost"} <DropdownMenuTrigger asChild>
size="icon" <Button variant={"ghost"} size="icon" aria-label={t`Toggle theme`}>
aria-label={t`Toggle theme`} <SunIcon className="h-[1.2rem] w-[1.2rem] dark:opacity-0" />
onClick={() => setTheme(theme === "dark" ? "light" : "dark")} <MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] opacity-0 dark:opacity-100" />
> </Button>
<SunIcon className="h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0" /> </DropdownMenuTrigger>
<MoonStarIcon className="absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0" /> <DropdownMenuContent>
</Button> {options.map((opt) => {
const selected = opt.theme === theme
return (
<DropdownMenuItem
key={opt.theme}
className={cn("px-2.5", selected ? "font-semibold" : "")}
onClick={() => setTheme(opt.theme as "dark" | "light" | "system")}
>
<opt.Icon className={cn("me-2 h-4 w-4 opacity-80", selected && "opacity-100")} />
{opt.label}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
) )
} }

View File

@@ -1,4 +1,4 @@
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro";
import { useState, lazy, Suspense } from "react" import { useState, lazy, Suspense } from "react"
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button"
import { import {
@@ -15,8 +15,8 @@ import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle" import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle" import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo" import { Logo } from "./logo"
import { cn, runOnce } from "@/lib/utils" import { pb } from "@/lib/stores"
import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api" import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -36,17 +36,12 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() { export default function Navbar() {
return ( 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"> <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 <Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
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" /> <Logo className="h-[1.1rem] md:h-5 fill-foreground" />
</Link> </Link>
<SearchButton /> <SearchButton />
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}> <div className="flex items-center ms-auto">
<LangToggle /> <LangToggle />
<ModeToggle /> <ModeToggle />
<Link <Link

View File

@@ -35,20 +35,18 @@ export const navigate = (urlString: string) => {
$router.open(urlString) $router.open(urlString)
} }
export function Link(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) { function onClick(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
return ( e.preventDefault()
<a $router.open(new URL((e.currentTarget as HTMLAnchorElement).href).pathname)
{...props} }
onClick={(e) => {
e.preventDefault() export const Link = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const href = props.href || "" let clickFn = onClick
if (e.ctrlKey || e.metaKey) { if (props.onClick) {
window.open(href, "_blank") clickFn = (e) => {
} else { onClick(e)
navigate(href) props.onClick?.(e)
props.onClick?.(e) }
} }
}} return <a {...props} onClick={clickFn}></a>
></a>
)
} }

View File

@@ -1,22 +1,19 @@
import { Suspense, memo, useEffect, useMemo } from "react" import { Suspense, lazy, memo, useEffect, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $systems } from "@/lib/stores" import { $alerts, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { GithubIcon } from "lucide-react" import { GithubIcon } from "lucide-react"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { getSystemNameFromId } from "@/lib/utils" import { alertInfo, getSystemNameFromId, updateRecordList, updateSystemList } from "@/lib/utils"
import { pb, updateRecordList, updateSystemList } from "@/lib/api"
import { AlertRecord, SystemRecord } from "@/types" import { AlertRecord, SystemRecord } from "@/types"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { $router, Link } from "../router" import { $router, Link } from "../router"
import { Plural, Trans, useLingui } from "@lingui/react/macro" import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { getPagePath } from "@nanostores/router" 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 () { export const Home = memo(() => {
const { t } = useLingui() const { t } = useLingui()
useEffect(() => { useEffect(() => {
@@ -44,7 +41,7 @@ export default memo(function () {
<SystemsTable /> <SystemsTable />
</Suspense> </Suspense>
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80"> <div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80">
<a <a
href="https://github.com/henrygd/beszel" href="https://github.com/henrygd/beszel"
target="_blank" target="_blank"
@@ -108,7 +105,7 @@ const ActiveAlerts = () => {
return ( return (
<Alert <Alert
key={alert.id} key={alert.id}
className="hover:-translate-y-px duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black/5" className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
> >
<info.icon className="h-4 w-4" /> <info.icon className="h-4 w-4" />
<AlertTitle> <AlertTitle>

View File

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

View File

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

View File

@@ -39,8 +39,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<form onSubmit={handleSubmit} className="space-y-5"> <form onSubmit={handleSubmit} className="space-y-5">
<div className="grid gap-2"> <div className="space-y-2">
<div className="mb-2"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium flex items-center gap-2"> <h3 className="mb-1 text-lg font-medium flex items-center gap-2">
<LanguagesIcon className="h-4 w-4" /> <LanguagesIcon className="h-4 w-4" />
<Trans>Language</Trans> <Trans>Language</Trans>
@@ -73,8 +73,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</Select> </Select>
</div> </div>
<Separator /> <Separator />
<div className="grid gap-2"> <div className="space-y-2">
<div className="mb-2"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">
<Trans>Chart options</Trans> <Trans>Chart options</Trans>
</h3> </h3>
@@ -102,8 +102,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</p> </p>
</div> </div>
<Separator /> <Separator />
<div className="grid gap-2"> <div className="space-y-2">
<div className="mb-2"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">
<Trans comment="Temperature / network units">Unit preferences</Trans> <Trans comment="Temperature / network units">Unit preferences</Trans>
</h3> </h3>
@@ -112,7 +112,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</p> </p>
</div> </div>
<div className="grid sm:grid-cols-3 gap-4"> <div className="grid sm:grid-cols-3 gap-4">
<div className="grid gap-2"> <div className="space-y-2">
<Label className="block" htmlFor="unitTemp"> <Label className="block" htmlFor="unitTemp">
<Trans>Temperature unit</Trans> <Trans>Temperature unit</Trans>
</Label> </Label>
@@ -134,7 +134,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-2"> <div className="space-y-2">
<Label className="block" htmlFor="unitNet"> <Label className="block" htmlFor="unitNet">
<Trans comment="Context: Bytes or bits">Network unit</Trans> <Trans comment="Context: Bytes or bits">Network unit</Trans>
</Label> </Label>
@@ -156,7 +156,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-2"> <div className="space-y-2">
<Label className="block" htmlFor="unitDisk"> <Label className="block" htmlFor="unitDisk">
<Trans>Disk unit</Trans> <Trans>Disk unit</Trans>
</Label> </Label>
@@ -181,8 +181,8 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</div> </div>
</div> </div>
<Separator /> <Separator />
<div className="grid gap-2"> <div className="space-y-2">
<div className="mb-2"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">
<Trans>Warning thresholds</Trans> <Trans>Warning thresholds</Trans>
</h3> </h3>
@@ -191,7 +191,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</p> </p>
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4 items-end"> <div className="grid grid-cols-2 lg:grid-cols-3 gap-4 items-end">
<div className="grid gap-2"> <div className="space-y-1">
<Label htmlFor="colorWarn"> <Label htmlFor="colorWarn">
<Trans>Warning (%)</Trans> <Trans>Warning (%)</Trans>
</Label> </Label>
@@ -205,7 +205,7 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
defaultValue={userSettings.colorWarn ?? 65} defaultValue={userSettings.colorWarn ?? 65}
/> />
</div> </div>
<div className="grid gap-1"> <div className="space-y-1">
<Label htmlFor="colorCrit"> <Label htmlFor="colorCrit">
<Trans>Critical (%)</Trans> <Trans>Critical (%)</Trans>
</Label> </Label>

View File

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

View File

@@ -3,6 +3,7 @@ import { Trans } from "@lingui/react/macro"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { pb } from "@/lib/stores"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react" import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
@@ -12,8 +13,8 @@ import { InputTags } from "@/components/ui/input-tags"
import { UserSettings } from "@/types" import { UserSettings } from "@/types"
import { saveSettings } from "./layout" import { saveSettings } from "./layout"
import * as v from "valibot" import * as v from "valibot"
import { isAdmin } from "@/lib/utils"
import { prependBasePath } from "@/components/router" import { prependBasePath } from "@/components/router"
import { isAdmin, pb } from "@/lib/api"
interface ShoutrrrUrlCardProps { interface ShoutrrrUrlCardProps {
url: string url: string
@@ -86,8 +87,8 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<div className="space-y-5"> <div className="space-y-5">
<div className="grid gap-2"> <div className="space-y-2">
<div className="mb-2"> <div className="mb-4">
<h3 className="mb-1 text-lg font-medium"> <h3 className="mb-1 text-lg font-medium">
<Trans>Email notifications</Trans> <Trans>Email notifications</Trans>
</h3> </h3>

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro" import { Plural, Trans } from "@lingui/react/macro"
import { import {
$systems, $systems,
pb,
$chartTime, $chartTime,
$containerFilter, $containerFilter,
$userSettings, $userSettings,
@@ -10,8 +11,8 @@ import {
$temperatureFilter, $temperatureFilter,
} from "@/lib/stores" } from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types" import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums" import { ChartType, Unit, Os } from "@/lib/enums"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react" import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card" import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import Spinner from "../spinner" import Spinner from "../spinner"
@@ -23,12 +24,12 @@ import {
decimalString, decimalString,
formatBytes, formatBytes,
getHostDisplayValue, getHostDisplayValue,
getPbTimestamp,
listen, listen,
parseSemVer, parseSemVer,
toFixedFloat, toFixedFloat,
useLocalStorage, useLocalStorage,
} from "@/lib/utils" } from "@/lib/utils"
import { getPbTimestamp, pb } from "@/lib/api"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import { Button } from "../ui/button" import { Button } from "../ui/button"
@@ -40,15 +41,15 @@ import { timeTicks } from "d3-time"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
import { $router, navigate } from "../router" import { $router, navigate } from "../router"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { batteryStateTranslations } from "@/lib/i18n"
import AreaChartDefault from "@/components/charts/area-chart" const AreaChartDefault = lazy(() => import("../charts/area-chart"))
import ContainerChart from "@/components/charts/container-chart" const ContainerChart = lazy(() => import("../charts/container-chart"))
import MemChart from "@/components/charts/mem-chart" const MemChart = lazy(() => import("../charts/mem-chart"))
import DiskChart from "@/components/charts/disk-chart" const DiskChart = lazy(() => import("../charts/disk-chart"))
import SwapChart from "@/components/charts/swap-chart" const SwapChart = lazy(() => import("../charts/swap-chart"))
import TemperatureChart from "@/components/charts/temperature-chart" const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
import GpuPowerChart from "@/components/charts/gpu-power-chart" const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
import LoadAverageChart from "@/components/charts/load-average-chart" const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
const cache = new Map<string, any>() const cache = new Map<string, any>()
@@ -285,11 +286,9 @@ export default function SystemDetail({ name }: { name: string }) {
value: system.info.k, value: system.info.k,
}, },
} }
let uptime: React.ReactNode let uptime: React.ReactNode
if (system.info.u < 3600) { if (system.info.u < 172800) {
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) const hours = Math.trunc(system.info.u / 3600)
uptime = <Plural value={hours} one="# hour" other="# hours" /> uptime = <Plural value={hours} one="# hour" other="# hours" />
} else { } else {
@@ -317,7 +316,7 @@ export default function SystemDetail({ name }: { name: string }) {
Icon: any Icon: any
hide?: boolean hide?: boolean
}[] }[]
}, [system.info, t]) }, [system.info])
/** Space for tooltip if more than 12 containers */ /** Space for tooltip if more than 12 containers */
useEffect(() => { useEffect(() => {
@@ -383,15 +382,15 @@ export default function SystemDetail({ name }: { name: string }) {
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined) const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined)
let translatedStatus: string = system.status let translatedStatus: string = system.status
if (system.status === SystemStatus.Up) { if (system.status === "up") {
translatedStatus = t({ message: "Up", comment: "Context: System is up" }) translatedStatus = t({ message: "Up", comment: "Context: System is up" })
} else if (system.status === SystemStatus.Down) { } else if (system.status === "down") {
translatedStatus = t({ message: "Down", comment: "Context: System is down" }) translatedStatus = t({ message: "Down", comment: "Context: System is down" })
} }
return ( return (
<> <>
<div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip"> <div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
{/* system info */} {/* system info */}
<Card> <Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5"> <div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -400,7 +399,7 @@ export default function SystemDetail({ name }: { name: string }) {
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90"> <div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<div className="capitalize flex gap-2 items-center"> <div className="capitalize flex gap-2 items-center">
<span className={cn("relative flex h-3 w-3")}> <span className={cn("relative flex h-3 w-3")}>
{system.status === SystemStatus.Up && ( {system.status === "up" && (
<span <span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }} style={{ animationDuration: "1.5s" }}
@@ -408,10 +407,10 @@ export default function SystemDetail({ name }: { name: string }) {
)} )}
<span <span
className={cn("relative inline-flex rounded-full h-3 w-3", { className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up, "bg-green-500": system.status === "up",
"bg-red-500": system.status === SystemStatus.Down, "bg-red-500": system.status === "down",
"bg-primary/40": system.status === SystemStatus.Paused, "bg-primary/40": system.status === "paused",
"bg-yellow-500": system.status === SystemStatus.Pending, "bg-yellow-500": system.status === "pending",
})} })}
></span> ></span>
</span> </span>
@@ -486,7 +485,7 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t`CPU Usage`, label: t`CPU Usage`,
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu), dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
color: 1, color: "1",
opacity: 0.4, opacity: 0.4,
}, },
]} ]}
@@ -512,9 +511,8 @@ export default function SystemDetail({ name }: { name: string }) {
grid={grid} grid={grid}
title={t`Memory Usage`} title={t`Memory Usage`}
description={t`Precise utilization at the recorded time`} description={t`Precise utilization at the recorded time`}
cornerEl={maxValSelect}
> >
<MemChart chartData={chartData} showMax={showMax} /> <MemChart chartData={chartData} />
</ChartCard> </ChartCard>
{containerFilterBar && ( {containerFilterBar && (
@@ -547,13 +545,13 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t({ message: "Write", comment: "Disk write" }), label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw), dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
color: 3, color: "3",
opacity: 0.3, opacity: 0.3,
}, },
{ {
label: t({ message: "Read", comment: "Disk read" }), label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr), dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
color: 1, color: "1",
opacity: 0.3, opacity: 0.3,
}, },
]} ]}
@@ -588,7 +586,7 @@ export default function SystemDetail({ name }: { name: string }) {
} }
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024 return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
}, },
color: 5, color: "5",
opacity: 0.2, opacity: 0.2,
}, },
{ {
@@ -599,7 +597,7 @@ export default function SystemDetail({ name }: { name: string }) {
} }
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024 return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
}, },
color: 2, color: "2",
opacity: 0.2, opacity: 0.2,
}, },
]} ]}
@@ -670,35 +668,6 @@ export default function SystemDetail({ name }: { name: string }) {
</ChartCard> </ChartCard>
)} )}
{/* Battery chart */}
{systemStats.at(-1)?.stats.bat && (
<ChartCard
empty={dataEmpty}
grid={grid}
title={t`Battery`}
description={`${t({
message: "Current state",
comment: "Context: Battery state",
})}: ${batteryStateTranslations[systemStats.at(-1)?.stats.bat![1] ?? 0]()}`}
>
<AreaChartDefault
chartData={chartData}
maxToggled={maxValues}
dataPoints={[
{
label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0],
color: 1,
opacity: 0.35,
},
]}
domain={[0, 100]}
tickFormatter={(val) => `${val}%`}
contentFormatter={({ value }) => `${value}%`}
/>
</ChartCard>
)}
{/* GPU power draw chart */} {/* GPU power draw chart */}
{hasGpuPowerData && ( {hasGpuPowerData && (
<ChartCard <ChartCard
@@ -731,7 +700,7 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t`Usage`, label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0, dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
color: 1, color: "1",
opacity: 0.35, opacity: 0.35,
}, },
]} ]}
@@ -751,7 +720,7 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t`Usage`, label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0, dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: 2, color: "2",
opacity: 0.25, opacity: 0.25,
}, },
]} ]}
@@ -803,13 +772,13 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t`Write`, label: t`Write`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0, dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0,
color: 3, color: "3",
opacity: 0.3, opacity: 0.3,
}, },
{ {
label: t`Read`, label: t`Read`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0, dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0,
color: 1, color: "1",
opacity: 0.3, opacity: 0.3,
}, },
]} ]}
@@ -903,10 +872,10 @@ function ChartCard({
return ( return (
<Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}> <Card className={cn("pb-2 sm:pb-4 odd:last-of-type:col-span-full", { "col-span-full": !grid })} ref={ref}>
<CardHeader className="pb-5 pt-4 gap-1 relative max-sm:py-3 max-sm:px-4"> <CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle> <CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription> <CardDescription>{description}</CardDescription>
{cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-3.5 sm:end-3.5">{cornerEl}</div>} {cornerEl && <div className="relative py-1 block sm:w-44 sm:absolute sm:top-2.5 sm:end-3.5">{cornerEl}</div>}
</CardHeader> </CardHeader>
<div className="ps-0 w-[calc(100%-1.5em)] h-48 md:h-52 relative group"> <div className="ps-0 w-[calc(100%-1.5em)] h-48 md:h-52 relative group">
{ {

View File

@@ -23,11 +23,12 @@ import {
formatBytes, formatBytes,
formatTemperature, formatTemperature,
getMeterState, getMeterState,
isReadOnlyUser,
parseSemVer, parseSemVer,
} from "@/lib/utils" } from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons" import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores" import { $userSettings, pb } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro" import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react" import { useMemo, useRef, useState } from "react"
import { memo } from "react" import { memo } from "react"
@@ -53,16 +54,13 @@ import {
} from "../ui/alert-dialog" } from "../ui/alert-dialog"
import { buttonVariants } from "../ui/button" import { buttonVariants } from "../ui/button"
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums" import { MeterState } from "@/lib/enums"
import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router"
import { isReadOnlyUser, pb } from "@/lib/api"
const STATUS_COLORS = { const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500", up: "bg-green-500",
[SystemStatus.Down]: "bg-red-500", down: "bg-red-500",
[SystemStatus.Paused]: "bg-primary/40", paused: "bg-primary/40",
[SystemStatus.Pending]: "bg-yellow-500", pending: "bg-yellow-500",
} as const } as const
/** /**
@@ -72,8 +70,7 @@ const STATUS_COLORS = {
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] { export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
return [ return [
{ {
// size: 200, size: 200,
size: 100,
minSize: 0, minSize: 0,
accessorKey: "name", accessorKey: "name",
id: "system", id: "system",
@@ -83,9 +80,9 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
let filterInputLower = "" let filterInputLower = ""
const nameCache = new Map<string, string>() const nameCache = new Map<string, string>()
const statusTranslations = { const statusTranslations = {
[SystemStatus.Up]: t`Up`.toLowerCase(), up: t`Up`.toLowerCase(),
[SystemStatus.Down]: t`Down`.toLowerCase(), down: t`Down`.toLowerCase(),
[SystemStatus.Paused]: t`Paused`.toLowerCase(), paused: t`Paused`.toLowerCase(),
} as const } as const
// match filter value against name or translated status // match filter value against name or translated status
@@ -110,26 +107,12 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
enableHiding: false, enableHiding: false,
invertSorting: false, invertSorting: false,
Icon: ServerIcon, Icon: ServerIcon,
cell: (info) => { cell: (info) => (
const { name } = info.row.original <span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5">
const longestName = useStore($longestSystemNameLen) <IndicatorDot system={info.row.original} />
return ( {info.getValue() as string}
<> </span>
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1"> ),
<IndicatorDot system={info.row.original} />
{/* 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 })}
className="inset-0 absolute size-full"
aria-label={name}
></Link>
</>
)
},
header: sortableHeader, header: sortableHeader,
}, },
{ {
@@ -191,7 +174,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
} }
const max = Math.max(...loadAverages) const max = Math.max(...loadAverages)
if (max === 0 && (status === SystemStatus.Paused || minor < 12)) { if (max === 0 && (status === "paused" || minor < 12)) {
return null return null
} }
@@ -202,10 +185,10 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight"> <div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
<span <span
className={cn("inline-block size-2 rounded-full me-0.5", { className={cn("inline-block size-2 rounded-full me-0.5", {
[STATUS_COLORS[SystemStatus.Up]]: threshold === MeterState.Good, [STATUS_COLORS.up]: threshold === MeterState.Good,
[STATUS_COLORS[SystemStatus.Pending]]: threshold === MeterState.Warn, [STATUS_COLORS.pending]: threshold === MeterState.Warn,
[STATUS_COLORS[SystemStatus.Down]]: threshold === MeterState.Crit, [STATUS_COLORS.down]: threshold === MeterState.Crit,
[STATUS_COLORS[SystemStatus.Paused]]: status !== SystemStatus.Up, [STATUS_COLORS.paused]: status !== "up",
})} })}
/> />
{loadAverages?.map((la, i) => ( {loadAverages?.map((la, i) => (
@@ -225,7 +208,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
cell(info) { cell(info) {
const sys = info.row.original const sys = info.row.original
const userSettings = useStore($userSettings, { keys: ["unitNet"] }) const userSettings = useStore($userSettings, { keys: ["unitNet"] })
if (sys.status === SystemStatus.Paused) { if (sys.status === "paused") {
return null return null
} }
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false) const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
@@ -274,13 +257,13 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
} }
const system = info.row.original const system = info.row.original
return ( return (
<span className={cn("flex gap-1.5 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}> <span className={cn("flex gap-2 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
<IndicatorDot <IndicatorDot
system={system} system={system}
className={ className={
(system.status !== SystemStatus.Up && STATUS_COLORS[SystemStatus.Paused]) || (system.status !== "up" && STATUS_COLORS.paused) ||
(version === globalThis.BESZEL.HUB_VERSION && STATUS_COLORS[SystemStatus.Up]) || (version === globalThis.BESZEL.HUB_VERSION && STATUS_COLORS.up) ||
STATUS_COLORS[SystemStatus.Pending] STATUS_COLORS.pending
} }
/> />
<span className="truncate max-w-14">{info.getValue() as string}</span> <span className="truncate max-w-14">{info.getValue() as string}</span>
@@ -294,7 +277,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
name: () => t({ message: "Actions", comment: "Table column" }), name: () => t({ message: "Actions", comment: "Table column" }),
size: 50, size: 50,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="relative z-10 flex justify-end items-center gap-1 -ms-3"> <div className="flex justify-end items-center gap-1 -ms-3">
<AlertButton system={row.original} /> <AlertButton system={row.original} />
<ActionsButton system={row.original} /> <ActionsButton system={row.original} />
</div> </div>
@@ -323,18 +306,22 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) { function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0 const val = Number(info.getValue()) || 0
const threshold = getMeterState(val) const threshold = getMeterState(val)
const meterClass = cn(
"h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)
return ( return (
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full"> <div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span> <span className="min-w-8">{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="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span className={meterClass} style={{ width: `${val}%` }}></span> <span
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== "up" && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
</span> </span>
</div> </div>
) )
@@ -344,7 +331,7 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
className ||= STATUS_COLORS[system.status as keyof typeof STATUS_COLORS] || "" className ||= STATUS_COLORS[system.status as keyof typeof STATUS_COLORS] || ""
return ( return (
<span <span
className={cn("shrink-0 size-2 rounded-full", className)} className={cn("flex-shrink-0 size-2 rounded-full", className)}
// style={{ marginBottom: "-1px" }} // style={{ marginBottom: "-1px" }}
/> />
) )
@@ -362,7 +349,7 @@ export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
<> <>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"}> <Button variant="ghost" size={"icon"} data-nolink>
<span className="sr-only"> <span className="sr-only">
<Trans>Open menu</Trans> <Trans>Open menu</Trans>
</span> </span>
@@ -385,11 +372,11 @@ export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
className={cn(isReadOnlyUser() && "hidden")} className={cn(isReadOnlyUser() && "hidden")}
onClick={() => { onClick={() => {
pb.collection("systems").update(id, { pb.collection("systems").update(id, {
status: status === SystemStatus.Paused ? SystemStatus.Pending : SystemStatus.Paused, status: status === "paused" ? "pending" : "paused",
}) })
}} }}
> >
{status === SystemStatus.Paused ? ( {status === "paused" ? (
<> <>
<PlayCircleIcon className="me-2.5 size-4" /> <PlayCircleIcon className="me-2.5 size-4" />
<Trans>Resume</Trans> <Trans>Resume</Trans>

View File

@@ -11,8 +11,11 @@ import {
Row, Row,
Table as TableType, Table as TableType,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@@ -33,59 +36,32 @@ import {
ArrowUpIcon, ArrowUpIcon,
Settings2Icon, Settings2Icon,
EyeIcon, EyeIcon,
FilterIcon,
} from "lucide-react" } from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react" import { memo, useEffect, useMemo, useState } from "react"
import { $systems } from "@/lib/stores" import { $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, runOnce, useLocalStorage } from "@/lib/utils" import { cn, useLocalStorage } from "@/lib/utils"
import { $router, Link } from "../router" import { $router, Link, navigate } from "../router"
import { useLingui, Trans } from "@lingui/react/macro" import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "@/components/ui/input" import { Input } from "../ui/input"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns" import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import AlertButton from "../alerts/alert-button" import AlertButton from "../alerts/alert-button"
import { SystemStatus } from "@/lib/enums"
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
type ViewMode = "table" | "grid" type ViewMode = "table" | "grid"
type StatusFilter = "all" | "up" | "down" | "paused"
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
export default function SystemsTable() { export default function SystemsTable() {
const data = useStore($systems) const data = useStore($systems)
const { i18n, t } = useLingui() const { i18n, t } = useLingui()
const [filter, setFilter] = useState<string>() const [filter, setFilter] = useState<string>()
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all") const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }])
const [sorting, setSorting] = useLocalStorage<SortingState>("sortMode",[{ id: "system", desc: false }])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {}) const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>( const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
"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 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])
const runningRecords = useMemo(() => {
return data.filter((record) => record.status === "up").length
}, [data])
const totalRecords = useMemo(() => {
return data.length
}, [data])
useEffect(() => { useEffect(() => {
if (filter !== undefined) { if (filter !== undefined) {
table.getColumn("system")?.setFilterValue(filter) table.getColumn("system")?.setFilterValue(filter)
@@ -95,7 +71,7 @@ export default function SystemsTable() {
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode]) const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode])
const table = useReactTable({ const table = useReactTable({
data: filteredData, data,
columns: columnDefs, columns: columnDefs,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting, onSortingChange: setSorting,
@@ -129,13 +105,11 @@ export default function SystemsTable() {
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2.5"> <CardTitle className="mb-2.5">
<Trans>All Systems</Trans> <Trans>All Systems</Trans>
</CardTitle> </CardTitle>
<CardDescription className="flex"> <CardDescription>
<Trans>Click on a system to view information - {runningRecords} / {totalRecords}</Trans> <Trans>Updated in real time. Click on a system to view information.</Trans>
<p className={"ml-2 text-" + (runningRecords === totalRecords ? "emerald" : "red") + "-600"}>Online</p>
</CardDescription> </CardDescription>
</div> </div>
<div className="flex gap-2 ms-auto w-full md:w-80"> <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" /> <Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<DropdownMenu> <DropdownMenu>
@@ -146,8 +120,8 @@ export default function SystemsTable() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="h-72 md:h-auto min-w-48 md:min-w-auto overflow-y-auto"> <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-4 divide-y md:divide-s md:divide-y-0"> <div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-s md:divide-y-0">
<div className="border-r"> <div>
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2"> <DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<LayoutGridIcon className="size-4" /> <LayoutGridIcon className="size-4" />
<Trans>Layout</Trans> <Trans>Layout</Trans>
@@ -169,33 +143,7 @@ export default function SystemsTable() {
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
</div> </div>
<div className="border-r"> <div>
<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"> <DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<ArrowUpDownIcon className="size-4" /> <ArrowUpDownIcon className="size-4" />
<Trans>Sort By</Trans> <Trans>Sort By</Trans>
@@ -261,7 +209,7 @@ export default function SystemsTable() {
</div> </div>
</CardHeader> </CardHeader>
) )
}, [visibleColumns.length, sorting, viewMode, locale, statusFilter, runningRecords, totalRecords]) }, [visibleColumns.length, sorting, viewMode, locale])
return ( return (
<Card> <Card>
@@ -269,7 +217,7 @@ export default function SystemsTable() {
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2"> <div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
{viewMode === "table" ? ( {viewMode === "table" ? (
// table layout // table layout
<div className="rounded-md"> <div className="rounded-md border overflow-hidden">
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} /> <AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
</div> </div>
) : ( ) : (
@@ -291,78 +239,36 @@ export default function SystemsTable() {
) )
} }
const AllSystemsTable = memo(function ({ const AllSystemsTable = memo(
table, ({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
rows, return (
colLength, <Table>
}: { <SystemsTableHead table={table} colLength={colLength} />
table: TableType<SystemRecord> <TableBody>
rows: Row<SystemRecord>[] {rows.length ? (
colLength: number rows.map((row) => (
}) { <SystemTableRow key={row.original.id} row={row} length={rows.length} colLength={colLength} />
// The virtualizer will need a reference to the scrollable container element ))
const scrollRef = useRef<HTMLDivElement>(null) ) : (
<TableRow>
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({ <TableCell colSpan={colLength} className="h-24 text-center">
count: rows.length, <Trans>No systems found.</Trans>
estimateSize: () => (rows.length > 10 ? 56 : 60), </TableCell>
getScrollElement: () => scrollRef.current, </TableRow>
overscan: 5, )}
}) </TableBody>
const virtualRows = virtualizer.getVirtualItems() </Table>
)
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 }) { function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
const { i18n } = useLingui() const { i18n } = useLingui()
return useMemo(() => { return useMemo(() => {
return ( return (
<TableHeader className="sticky top-0 z-20 w-full border-b-2"> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead className="px-1.5" key={header.id}> <TableHead className="px-1.5" key={header.id}>
@@ -370,49 +276,47 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
</TableHead> </TableHead>
) )
})} })}
</tr> </TableRow>
))} ))}
</TableHeader> </TableHeader>
) )
}, [i18n.locale, colLength]) }, [i18n.locale, colLength])
} }
const SystemTableRow = memo(function ({ const SystemTableRow = memo(
row, ({ row, length, colLength }: { row: Row<SystemRecord>; length: number; colLength: number }) => {
virtualRow, const system = row.original
colLength, const { t } = useLingui()
}: { return useMemo(() => {
row: Row<SystemRecord> return (
virtualRow: VirtualItem <TableRow
length: number // data-state={row.getIsSelected() && "selected"}
colLength: number className={cn("cursor-pointer transition-opacity", {
}) { "opacity-50": system.status === "paused",
const system = row.original })}
const { t } = useLingui() onClick={(e) => {
return useMemo(() => { const target = e.target as HTMLElement
return ( if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
<TableRow navigate(getPagePath($router, "system", { name: system.name }))
// 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
{row.getVisibleCells().map((cell) => ( key={cell.id}
<TableCell style={{
key={cell.id} width: cell.column.getSize(),
style={{ }}
width: cell.column.getSize(), className={cn("overflow-hidden relative", length > 10 ? "py-2" : "py-2.5")}
height: virtualRow.size, >
}} {flexRender(cell.column.columnDef.cell, cell.getContext())}
className="py-0" </TableCell>
> ))}
{flexRender(cell.column.columnDef.cell, cell.getContext())} </TableRow>
</TableCell> )
))} }, [system, system.status, colLength, t])
</TableRow> }
) )
}, [system, system.status, colLength, t])
})
const SystemCard = memo( const SystemCard = memo(
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => { ({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
@@ -422,58 +326,49 @@ const SystemCard = memo(
return useMemo(() => { return useMemo(() => {
return ( return (
<Card <Card
onMouseEnter={preloadSystemDetail}
key={system.id} key={system.id}
className={cn( className={cn(
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative", "cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
{ {
"opacity-50": system.status === SystemStatus.Paused, "opacity-50": system.status === "paused",
} }
)} )}
> >
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60"> <CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
<div className="flex items-center gap-2 w-full overflow-hidden"> <div className="flex items-center justify-between gap-2">
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5"> <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 flex-1"> <div className="flex items-center gap-2.5 min-w-0">
<IndicatorDot system={system} /> <IndicatorDot system={system} />
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span> <CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
{system.name}
</CardTitle>
</div> </div>
</CardTitle> </CardTitle>
{table.getColumn("actions")?.getIsVisible() && ( {table.getColumn("actions")?.getIsVisible() && (
<div className="flex gap-1 shrink-0 relative z-10"> <div className="flex gap-1 flex-shrink-0 relative z-10">
<AlertButton system={system} /> <AlertButton system={system} />
<ActionsButton system={system} /> <ActionsButton system={system} />
</div> </div>
)} )}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="text-sm px-5 pt-3.5 pb-4"> <CardContent className="space-y-2.5 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) => {
{table.getAllColumns().map((column) => { if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
const cell = row.getAllCells().find((cell) => cell.column.id === column.id) if (!cell) return null
if (!cell) return null // @ts-ignore
// @ts-ignore const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown> return (
return ( <div key={column.id} className="flex items-center gap-3">
<> {Icon && <Icon className="size-4 text-muted-foreground" />}
<div key={`${column.id}-icon`} className="flex items-center"> <div className="flex items-center gap-3 flex-1">
{column.id === "lastSeen" ? ( <span className="text-muted-foreground min-w-16">{name()}:</span>
<EyeIcon className="size-4 text-muted-foreground" /> <div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
) : ( </div>
Icon && <Icon className="size-4 text-muted-foreground" /> </div>
)} )
</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> </CardContent>
<Link <Link
href={getPagePath($router, "system", { name: row.original.name })} href={getPagePath($router, "system", { name: row.original.name })}

View File

@@ -34,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}
@@ -44,7 +44,7 @@ const AlertDialogContent = React.forwardRef<
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-2 text-center sm:text-start", className)} {...props} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-start", className)} {...props} />
) )
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader"

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -5,14 +5,16 @@ import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("rounded-lg border border-border/60 bg-card text-card-foreground shadow-xs", className)} className={cn("rounded-lg border border-border/60 bg-card text-card-foreground shadow-sm", className)}
{...props} {...props}
/> />
)) ))
Card.displayName = "Card" Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("grid gap-1.5 p-6", className)} {...props} /> ({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
)
) )
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader"

View File

@@ -4,8 +4,6 @@ import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from "@/lib/utils" import { chartTimeData, cn } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import type { JSX } from "react"
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const
@@ -44,12 +42,11 @@ const ChartContainer = React.forwardRef<
return ( return (
//<ChartContext.Provider value={{ config }}> //<ChartContext.Provider value={{ config }}>
//</ChartContext.Provider>
<div <div
data-chart={chartId} data-chart={chartId}
ref={ref} ref={ref}
className={cn( className={cn(
"text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden", "text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className className
)} )}
{...props} {...props}
@@ -57,6 +54,7 @@ const ChartContainer = React.forwardRef<
{/* <ChartStyle id={chartId} config={config} /> */} {/* <ChartStyle id={chartId} config={config} /> */}
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer> <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div> </div>
//</ChartContext.Provider>
) )
}) })
ChartContainer.displayName = "Chart" ChartContainer.displayName = "Chart"
@@ -171,7 +169,7 @@ const ChartTooltipContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"grid min-w-28 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", "grid min-w-[7rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className className
)} )}
> >
@@ -198,7 +196,7 @@ const ChartTooltipContent = React.forwardRef<
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
<div <div
className={cn("shrink-0 rounded-[2px] border-border bg-(--color-bg)", { className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot", "h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line", "w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed", "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
@@ -228,7 +226,7 @@ const ChartTooltipContent = React.forwardRef<
{itemConfig?.label || item.name} {itemConfig?.label || item.name}
</span> </span>
{item.value !== undefined && ( {item.value !== undefined && (
<span className="font-medium text-foreground"> <span className="font-medium tabular-nums text-foreground">
{content && typeof content === "function" {content && typeof content === "function"
? content(item, key) ? content(item, key)
: item.value.toLocaleString() + (unit ? unit : "")} : item.value.toLocaleString() + (unit ? unit : "")}
@@ -267,7 +265,7 @@ const ChartLegendContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"flex items-center justify-center gap-4 gap-y-1 flex-wrap ps-4", "flex items-center justify-center gap-4 gap-y-1 flex-wrap",
verticalAlign === "top" ? "pb-3" : "pt-3", verticalAlign === "top" ? "pb-3" : "pt-3",
className className
)} )}

View File

@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer size-4 flex items-center justify-center shrink-0 rounded-[.3em] border border-input ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "peer size-4 flex items-center justify-center shrink-0 rounded-[.3em] border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className className
)} )}

View File

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

View File

@@ -36,13 +36,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
@@ -52,7 +52,7 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 text-center sm:text-start", className)} {...props} /> <div className={cn("flex flex-col space-y-1.5 text-center sm:text-start", className)} {...props} />
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"

View File

@@ -25,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 data-[state=open]:bg-accent/70", "flex cursor-default select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-none focus:bg-accent/70 data-[state=open]:bg-accent/70",
inset && "ps-8", inset && "ps-8",
className className
)} )}
@@ -44,7 +44,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 className
)} )}
{...props} {...props}
@@ -61,7 +61,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 className
)} )}
{...props} {...props}
@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"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", "relative flex cursor-default select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-none focus:bg-accent/70 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "ps-8", inset && "ps-8",
className className
)} )}
@@ -95,7 +95,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"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", "relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none focus:bg-accent/70 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
checked={checked} checked={checked}
@@ -118,7 +118,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"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", "relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none focus:bg-accent/70 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...props} {...props}

View File

@@ -11,11 +11,11 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
<Input readOnly id={id} name={name} value={value} required></Input> <Input readOnly id={id} name={name} value={value} required></Input>
<div <div
className={ className={
"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none" "h-6 w-24 bg-gradient-to-r rtl:bg-gradient-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
} }
></div> ></div>
<TooltipProvider delayDuration={100} disableHoverableContent> <TooltipProvider delayDuration={100} disableHoverableContent>
<Tooltip disableHoverableContent={true}> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
type="button" type="button"

View File

@@ -33,7 +33,7 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
return ( return (
<div <div
className={cn( className={cn(
"bg-background min-h-10 flex w-full flex-wrap gap-2 rounded-md border px-3 py-2 text-sm placeholder:text-muted-foreground has-focus-visible:outline-hidden ring-offset-background has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "bg-background min-h-10 flex w-full flex-wrap gap-2 rounded-md border px-3 py-2 text-sm placeholder:text-muted-foreground has-[:focus-visible]:outline-none ring-offset-background has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
> >
@@ -53,7 +53,7 @@ const InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(
</Badge> </Badge>
))} ))}
<input <input
className="flex-1 outline-hidden bg-background placeholder:text-muted-foreground" className="flex-1 outline-none bg-background placeholder:text-muted-foreground"
value={pendingDataPoint} value={pendingDataPoint}
onChange={(e) => setPendingDataPoint(e.target.value)} onChange={(e) => setPendingDataPoint(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {

View File

@@ -2,19 +2,21 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) { export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
data-slot="input"
className={cn( 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", "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-none 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 className
)} )}
ref={ref}
{...props} {...props}
/> />
) )
} })
Input.displayName = "Input"
export { Input } export { Input }

View File

@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className
)} )}
{...props} {...props}
@@ -66,7 +66,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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", "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
@@ -79,7 +79,7 @@ const SelectContent = React.forwardRef<
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)} )}
> >
{children} {children}
@@ -105,7 +105,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full 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 w-full cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none focus:bg-accent/70 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...props} {...props}

View File

@@ -11,7 +11,7 @@ const Separator = React.forwardRef<
ref={ref} ref={ref}
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-px w-full" : "h-full w-px", className)} className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
{...props} {...props}
/> />
)) ))

View File

@@ -1,101 +0,0 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in duration-500 isolate data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/40",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-[400ms]",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
}
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }

View File

@@ -15,7 +15,7 @@ const Slider = React.forwardRef<
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" /> <SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track> </SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" /> <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root> </SliderPrimitive.Root>
)) ))
Slider.displayName = SliderPrimitive.Root.displayName Slider.displayName = SliderPrimitive.Root.displayName

View File

@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className className
)} )}
{...props} {...props}
@@ -17,7 +17,7 @@ const Switch = React.forwardRef<
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=checked]:rtl:-translate-x-5 data-[state=unchecked]:translate-x-0" "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 rtl:data-[state=checked]:-translate-x-5 data-[state=unchecked]:translate-x-0"
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>

View File

@@ -13,11 +13,7 @@ Table.displayName = "Table"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>( const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<thead <thead ref={ref} className={cn("bg-muted/30 [&_tr]:border-b", className)} {...props} />
ref={ref}
className={cn("bg-table-header border-b border-border/50 [&_tr]:border-b", className)}
{...props}
/>
) )
) )
TableHeader.displayName = "TableHeader" TableHeader.displayName = "TableHeader"
@@ -31,7 +27,7 @@ TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>( const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium last:[&>tr]:border-b-0", className)} {...props} /> <tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
) )
) )
TableFooter.displayName = "TableFooter" TableFooter.displayName = "TableFooter"
@@ -41,7 +37,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:bg-muted!", "border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:!bg-muted",
className className
)} )}
{...props} {...props}

View File

@@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs cursor-pointer", "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className className
)} )}
{...props} {...props}
@@ -42,7 +42,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className
)} )}
{...props} {...props}

View File

@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ classNa
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-14 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background 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", "flex min-h-14 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
ref={ref} ref={ref}

View File

@@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-100 flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", "fixed top-0 z-[100] flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className className
)} )}
{...props} {...props}
@@ -23,7 +23,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pe-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full", "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pe-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{ {
variants: { variants: {
variant: { variant: {
@@ -52,7 +52,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground focus:group-[.destructive]:ring-destructive", "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className className
)} )}
{...props} {...props}
@@ -67,7 +67,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
className={cn( className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600", "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className className
)} )}
toast-close="" toast-close=""

View File

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

View File

@@ -1,162 +1,101 @@
@import "tailwindcss"; @tailwind base;
@import "tw-animate-css"; @tailwind components;
@tailwind utilities;
@custom-variant light (&:is(.light *)); @layer base {
@custom-variant dark (&:is(.dark *)); :root {
@custom-variant safari (@supports (hanging-punctuation: first) and (-webkit-appearance: none)); --background: 30 8% 98%;
--foreground: 30 0% 0%;
:root { --card: 30 0% 100%;
--background: hsl(30 8% 98%); --card-foreground: 240 6.67% 2.94%;
--foreground: hsl(30 0% 10%); --popover: 30 0% 100%;
--card: hsl(30 0% 100%); --popover-foreground: 240 10% 6.2%;
--card-foreground: hsl(240 6% 12%); --primary: 240 5.88% 10%;
--popover: hsl(30 0% 100%); --primary-foreground: 30 0% 100%;
--popover-foreground: hsl(240 10% 6.2%); --secondary: 240 4.76% 95.88%;
--primary: hsl(240 5.88% 10%); --secondary-foreground: 240 5.88% 10%;
--primary-foreground: hsl(30 0% 100%); --muted: 26 6% 94%;
--secondary: hsl(240 4.76% 95.88%); --muted-foreground: 24 2.79% 35.1%;
--secondary-foreground: hsl(240 5.88% 10%); --accent: 20 23.08% 94%;
--muted: hsl(26 6% 94%); --accent-foreground: 240 5.88% 10%;
--muted-foreground: hsl(24 2.79% 35.1%); --destructive: 0 66% 53%;
--accent: hsl(20 23.08% 94%); --destructive-foreground: 0 0% 98.04%;
--accent-foreground: hsl(240 5.88% 10%); --border: 30 8.11% 85.49%;
--destructive: hsl(0 66% 53%); --input: 30 4.29% 72.55%;
--destructive-foreground: hsl(0 0% 97%); --ring: 30 3.97% 49.41%;
--border: hsl(30 8.11% 85.49%); --radius: 0.8rem;
--input: hsl(30 4.29% 72.55%); /* charts */
--ring: hsl(30 3.97% 49.41%); --chart-1: 220 70% 50%;
--radius: 0.8rem; --chart-2: 160 60% 45%;
--chart-1: hsl(220 70% 50%); --chart-3: 30 80% 55%;
--chart-2: hsl(160 60% 45%); --chart-4: 280 65% 60%;
--chart-3: hsl(30 80% 55%); --chart-5: 340 75% 55%;
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--table-header: hsl(225, 6%, 97%);
}
.dark {
color-scheme: dark;
--background: hsl(220 5.5% 9%);
--foreground: hsl(220 2% 97%);
--card: hsl(220 5.5% 10.5%);
--card-foreground: hsl(220 2% 97%);
--popover: hsl(220 5.5% 9%);
--popover-foreground: hsl(220 2% 97%);
--primary: hsl(220 2% 96%);
--primary-foreground: hsl(220 4% 10%);
--secondary: hsl(220 4% 16%);
--secondary-foreground: hsl(220 0% 98%);
--muted: hsl(220 6% 16%);
--muted-foreground: hsl(220 4% 67%);
--accent: hsl(220 5% 15.5%);
--accent-foreground: hsl(220 2% 98%);
--destructive: hsl(0 62% 46%);
--border: hsl(220 3% 16%);
--input: hsl(220 4% 22%);
--ring: hsl(220 4% 80%);
--table-header: hsl(220, 6%, 13%);
--radius: 0.8rem;
}
@theme inline {
--font-sans: Inter, InterVariable, sans-serif;
--breakpoint-xs: 26.6rem;
--breakpoint-450: 28rem;
--breakpoint-2xl: 90rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-green-50: hsl(140 60% 95%);
--color-green-100: hsl(140 50% 90%);
--color-green-200: hsl(140 49% 80%);
--color-green-300: hsl(140 48% 70%);
--color-green-400: hsl(140 49% 60%);
--color-green-500: hsl(140 50% 48%);
--color-green-600: hsl(140 52% 38%);
--color-green-700: hsl(140 53% 29%);
--color-green-800: hsl(140 54% 20%);
--color-green-900: hsl(140 54% 12%);
--color-green-950: hsl(140 57% 6%);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--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);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--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 {
/* Fonts */
@supports (font-variation-settings: normal) {
:root {
font-family: Inter, InterVariable, sans-serif;
}
} }
@font-face {
font-family: InterVariable; .dark {
font-style: normal; color-scheme: dark;
font-weight: 100 900; --background: 220 5.5% 9%;
font-display: swap; --foreground: 220 2% 97%;
src: url("/static/InterVariable.woff2?v=4.0") format("woff2"); --card: 220 5.5% 10.5%;
--card-foreground: 220 2% 97%;
--popover: 220 5.5% 9%;
--popover-foreground: 220 2% 97%;
--primary: 220 2% 96%;
--primary-foreground: 220 4% 10%;
--secondary: 220 4% 16%;
--secondary-foreground: 220 0% 98%;
--muted: 220 6% 16%;
--muted-foreground: 220 4% 67%;
--accent: 220 5% 15.5%;
--accent-foreground: 220 2% 98%;
--destructive: 0 62% 46%;
--destructive-foreground: 0 0% 97%;
--border: 220 3% 16%;
--input: 220 4% 22%;
--ring: 220 4% 80%;
--radius: 0.8rem;
} }
} }
/* Fonts */
@supports (font-variation-settings: normal) {
:root {
font-family: Inter, InterVariable, sans-serif;
}
}
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/static/InterVariable.woff2?v=4.0") format("woff2");
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border;
overflow-anchor: none; overflow-anchor: none;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
button { }
cursor: pointer;
@layer utilities {
.link {
@apply text-primary font-medium underline-offset-4 hover:underline;
} }
}
@utility container {
@apply max-w-360 mx-auto px-4;
}
@utility link {
@apply text-primary font-medium underline-offset-4 hover:underline;
}
@utility ns-dialog {
/* New system dialog width */ /* New system dialog width */
min-width: 30.3rem; .ns-dialog {
:where(:lang(zh), :lang(zh-CN), :lang(ko)) & { min-width: 30.3rem;
}
:where(:lang(zh), :lang(zh-CN), :lang(ko)) .ns-dialog {
min-width: 27.9rem; min-width: 27.9rem;
} }
} }
.recharts-tooltip-wrapper { .recharts-tooltip-wrapper {
z-index: 1; z-index: 1;
@apply tabular-nums;
} }
.recharts-yAxis { .recharts-yAxis {

View File

@@ -1,171 +0,0 @@
import type { AlertInfo, AlertRecord } from "@/types"
import type { RecordSubscription } from "pocketbase"
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> = {
Status: {
name: () => t`Status`,
unit: "",
icon: ServerIcon,
desc: () => t`Triggers when status switches between up and down`,
/** "for x minutes" is appended to desc when only one value */
singleDesc: () => t`System` + " " + t`Down`,
},
CPU: {
name: () => t`CPU Usage`,
unit: "%",
icon: CpuIcon,
desc: () => t`Triggers when CPU usage exceeds a threshold`,
},
Memory: {
name: () => t`Memory Usage`,
unit: "%",
icon: MemoryStickIcon,
desc: () => t`Triggers when memory usage exceeds a threshold`,
},
Disk: {
name: () => t`Disk Usage`,
unit: "%",
icon: HardDriveIcon,
desc: () => t`Triggers when usage of any disk exceeds a threshold`,
},
Bandwidth: {
name: () => t`Bandwidth`,
unit: " MB/s",
icon: EthernetIcon,
desc: () => t`Triggers when combined up/down exceeds a threshold`,
max: 125,
},
Temperature: {
name: () => t`Temperature`,
unit: "°C",
icon: ThermometerIcon,
desc: () => t`Triggers when any sensor exceeds a threshold`,
},
LoadAvg1: {
name: () => t`Load Average 1m`,
unit: "",
icon: HourglassIcon,
max: 100,
min: 0.1,
start: 10,
step: 0.1,
desc: () => t`Triggers when 1 minute load average exceeds a threshold`,
},
LoadAvg5: {
name: () => t`Load Average 5m`,
unit: "",
icon: HourglassIcon,
max: 100,
min: 0.1,
start: 10,
step: 0.1,
desc: () => t`Triggers when 5 minute load average exceeds a threshold`,
},
LoadAvg15: {
name: () => t`Load Average 15m`,
unit: "",
icon: HourglassIcon,
min: 0.1,
max: 100,
start: 10,
step: 0.1,
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
},
} as const
/** Helper to manage user alerts */
export const alertManager = (() => {
const collection = pb.collection<AlertRecord>("alerts")
let unsub: () => void
/** Fields to fetch from alerts collection */
const fields = "id,name,system,value,min,triggered"
/** Fetch alerts from collection */
async function fetchAlerts(): Promise<AlertRecord[]> {
return await collection.getFullList<AlertRecord>({ fields, sort: "updated" })
}
/** Format alerts into a map of system id to alert name to alert record */
function add(alerts: AlertRecord[]) {
for (const alert of alerts) {
const systemId = alert.system
const systemAlerts = $alerts.get()[systemId] ?? new Map()
const newAlerts = new Map(systemAlerts)
newAlerts.set(alert.name, alert)
$alerts.setKey(systemId, newAlerts)
}
}
function remove(alerts: Pick<AlertRecord, "name" | "system">[]) {
for (const alert of alerts) {
const systemId = alert.system
const systemAlerts = $alerts.get()[systemId]
const newAlerts = new Map(systemAlerts)
newAlerts.delete(alert.name)
$alerts.setKey(systemId, newAlerts)
}
}
const actionFns = {
create: add,
update: add,
delete: remove,
}
// batch alert updates to prevent unnecessary re-renders when adding many alerts at once
const batchUpdate = (() => {
const batch = new Map<string, RecordSubscription<AlertRecord>>()
let timeout: ReturnType<typeof setTimeout>
return (data: RecordSubscription<AlertRecord>) => {
const { record } = data
batch.set(`${record.system}${record.name}`, data)
clearTimeout(timeout!)
timeout = setTimeout(() => {
const groups = { create: [], update: [], delete: [] } as Record<string, AlertRecord[]>
for (const { action, record } of batch.values()) {
groups[action]?.push(record)
}
for (const key in groups) {
if (groups[key].length) {
actionFns[key as keyof typeof actionFns]?.(groups[key])
}
}
batch.clear()
}, 50)
}
})()
async function subscribe() {
unsub = await collection.subscribe("*", batchUpdate, { fields })
}
function unsubscribe() {
unsub?.()
}
async function refresh() {
const records = await fetchAlerts()
add(records)
}
return {
/** Add alerts to store */
add,
/** Remove alerts from store */
remove,
/** Subscribe to alerts */
subscribe,
/** Unsubscribe from alerts */
unsubscribe,
/** Refresh alerts with latest data from hub */
refresh,
}
})()

View File

@@ -1,136 +0,0 @@
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

@@ -28,21 +28,3 @@ export enum MeterState {
Warn, Warn,
Crit, Crit,
} }
/** System status states */
export enum SystemStatus {
Up = "up",
Down = "down",
Pending = "pending",
Paused = "paused",
}
/** Battery state */
export enum BatteryState {
Unknown,
Empty,
Full,
Charging,
Discharging,
Idle,
}

View File

@@ -4,8 +4,6 @@ import type { Messages } from "@lingui/core"
import languages from "@/lib/languages" import languages from "@/lib/languages"
import { detect, fromStorage, fromNavigator } from "@lingui/detect-locale" import { detect, fromStorage, fromNavigator } from "@lingui/detect-locale"
import { messages as enMessages } from "@/locales/en/en" import { messages as enMessages } from "@/locales/en/en"
import { BatteryState } from "./enums"
import { t } from "@lingui/core/macro"
// activates locale // activates locale
function activateLocale(locale: string, messages: Messages = enMessages) { function activateLocale(locale: string, messages: Messages = enMessages) {
@@ -56,14 +54,3 @@ export function getLocale() {
} }
return locale return locale
} }
////////////////////////////////////////////////////////
export const batteryStateTranslations = {
[BatteryState.Unknown]: () => t({ message: "Unknown", comment: "Context: Battery state" }),
[BatteryState.Empty]: () => t({ message: "Empty", comment: "Context: Battery state" }),
[BatteryState.Full]: () => t({ message: "Full", comment: "Context: Battery state" }),
[BatteryState.Charging]: () => t({ message: "Charging", comment: "Context: Battery state" }),
[BatteryState.Discharging]: () => t({ message: "Discharging", comment: "Context: Battery state" }),
[BatteryState.Idle]: () => t({ message: "Idle", comment: "Context: Battery state" }),
} as const

View File

@@ -1,7 +1,11 @@
import PocketBase from "pocketbase"
import { atom, map } from "nanostores" import { atom, map } from "nanostores"
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types" import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { basePath } from "@/components/router"
import { Unit } from "./enums" import { Unit } from "./enums"
import { pb } from "./api"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
/** Store if user is authenticated */ /** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid) export const $authenticated = atom(pb.authStore.isValid)
@@ -53,8 +57,3 @@ export const $copyContent = atom("")
/** Direction for localization */ /** Direction for localization */
export const $direction = atom<"ltr" | "rtl">("ltr") 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

@@ -84,7 +84,7 @@ export function useIntersectionObserver({
entry: undefined, entry: undefined,
})) }))
const callbackRef = useRef<UseIntersectionObserverOptions["onChange"]>(undefined) const callbackRef = useRef<UseIntersectionObserverOptions["onChange"]>()
callbackRef.current = onChange callbackRef.current = onChange

View File

@@ -2,12 +2,25 @@ import { t } from "@lingui/core/macro"
import { toast } from "@/components/ui/use-toast" import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { $copyContent, $systems, $userSettings } from "./stores" import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types" import {
AlertInfo,
AlertRecord,
ChartTimeData,
ChartTimes,
FingerprintRecord,
SemVer,
SystemRecord,
UserSettings,
} from "@/types"
import { RecordModel, RecordSubscription } from "pocketbase"
import { WritableAtom } from "nanostores"
import { timeDay, timeHour } from "d3-time" import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { MeterState, Unit } from "./enums" import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
import { prependBasePath } from "@/components/router" import { prependBasePath } from "@/components/router"
import { MeterState, Unit } from "./enums"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -32,6 +45,52 @@ 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, { const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
@@ -62,6 +121,47 @@ export const updateFavicon = (newIcon: string) => {
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`) ;(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 = { export const chartTimeData: ChartTimeData = {
"1h": { "1h": {
type: "1m", type: "1m",
@@ -240,8 +340,99 @@ 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 } export const chartMargin = { top: 12 }
/** Alert info for each alert type */
export const alertInfo: Record<string, AlertInfo> = {
Status: {
name: () => t`Status`,
unit: "",
icon: ServerIcon,
desc: () => t`Triggers when status switches between up and down`,
/** "for x minutes" is appended to desc when only one value */
singleDesc: () => t`System` + " " + t`Down`,
},
CPU: {
name: () => t`CPU Usage`,
unit: "%",
icon: CpuIcon,
desc: () => t`Triggers when CPU usage exceeds a threshold`,
},
Memory: {
name: () => t`Memory Usage`,
unit: "%",
icon: MemoryStickIcon,
desc: () => t`Triggers when memory usage exceeds a threshold`,
},
Disk: {
name: () => t`Disk Usage`,
unit: "%",
icon: HardDriveIcon,
desc: () => t`Triggers when usage of any disk exceeds a threshold`,
},
Bandwidth: {
name: () => t`Bandwidth`,
unit: " MB/s",
icon: EthernetIcon,
desc: () => t`Triggers when combined up/down exceeds a threshold`,
max: 125,
},
Temperature: {
name: () => t`Temperature`,
unit: "°C",
icon: ThermometerIcon,
desc: () => t`Triggers when any sensor exceeds a threshold`,
},
LoadAvg1: {
name: () => t`Load Average 1m`,
unit: "",
icon: HourglassIcon,
max: 100,
min: 0.1,
start: 10,
step: 0.1,
desc: () => t`Triggers when 1 minute load average exceeds a threshold`,
},
LoadAvg5: {
name: () => t`Load Average 5m`,
unit: "",
icon: HourglassIcon,
max: 100,
min: 0.1,
start: 10,
step: 0.1,
desc: () => t`Triggers when 5 minute load average exceeds a threshold`,
},
LoadAvg15: {
name: () => t`Load Average 15m`,
unit: "",
icon: HourglassIcon,
min: 0.1,
max: 100,
start: 10,
step: 0.1,
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
},
} as const
/** /**
* Retuns value of system host, truncating full path if socket. * Retuns value of system host, truncating full path if socket.
* @example * @example
@@ -250,29 +441,8 @@ export const chartMargin = { top: 12 }
*/ */
export const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf("/") + 1) 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 */ /** Generate a random token for the agent */
export const generateToken = () => { export const generateToken = () => crypto?.randomUUID() ?? (performance.now() * Math.random()).toString(16)
try {
return crypto?.randomUUID()
} catch (e) {
return Array.from({ length: 2 }, () => (performance.now() * Math.random()).toString(16).replace(".", "-")).join("-")
}
}
/** Get the hub URL from the global BESZEL object */ /** Get the hub URL from the global BESZEL object */
export const getHubURL = () => BESZEL?.HUB_URL || window.location.origin export const getHubURL = () => BESZEL?.HUB_URL || window.location.origin
@@ -357,15 +527,81 @@ export const getSystemNameFromId = (() => {
} }
})() })()
/** Run a function only once */ // TODO: reorganize this utils file into more specific files
export function runOnce<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T> { /** Helper to manage user alerts */
let done = false export const alertManager = (() => {
let result: any const collection = pb.collection<AlertRecord>("alerts")
return (...args: any) => {
if (!done) { /** Fields to fetch from alerts collection */
result = fn(...args) const fields = "id,name,system,value,min,triggered"
done = true
} /** Fetch alerts from collection */
return result async function fetchAlerts(): Promise<AlertRecord[]> {
return await collection.getFullList<AlertRecord>({ fields, sort: "updated" })
} }
}
/** Format alerts into a map of system id to alert name to alert record */
function add(alerts: AlertRecord[]) {
for (const alert of alerts) {
const systemId = alert.system
const systemAlerts = $alerts.get()[systemId] ?? new Map()
const newAlerts = new Map(systemAlerts)
newAlerts.set(alert.name, alert)
$alerts.setKey(systemId, newAlerts)
}
}
function remove(alerts: Pick<AlertRecord, "name" | "system">[]) {
for (const alert of alerts) {
const systemId = alert.system
const systemAlerts = $alerts.get()[systemId]
const newAlerts = new Map(systemAlerts)
newAlerts.delete(alert.name)
$alerts.setKey(systemId, newAlerts)
}
}
const actionFns = {
create: add,
update: add,
delete: remove,
}
// batch alert updates to prevent unnecessary re-renders when adding many alerts at once
const batchUpdate = (() => {
const batch = new Map<string, RecordSubscription<AlertRecord>>()
let timeout: ReturnType<typeof setTimeout>
return (data: RecordSubscription<AlertRecord>) => {
const { record } = data
batch.set(`${record.system}${record.name}`, data)
clearTimeout(timeout!)
timeout = setTimeout(() => {
const groups = { create: [], update: [], delete: [] } as Record<string, AlertRecord[]>
for (const { action, record } of batch.values()) {
groups[action]?.push(record)
}
for (const key in groups) {
if (groups[key].length) {
actionFns[key as keyof typeof actionFns]?.(groups[key])
}
}
batch.clear()
}, 50)
}
})()
collection.subscribe("*", batchUpdate, { fields })
return {
/** Add alerts to store */
add,
/** Remove alerts from store */
remove,
/** Refresh alerts with latest data from hub */
async refresh() {
const records = await fetchAlerts()
add(records)
},
}
})()

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n" "Language: ar\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-25 01:15\n" "PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
@@ -33,10 +33,6 @@ msgstr "تم تحديد {0} من {1} صف"
msgid "{hours, plural, one {# hour} other {# hours}}" msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعة} other {# ساعات}}" msgstr "{hours, plural, one {# ساعة} other {# ساعات}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 ساعة" msgstr "1 ساعة"
@@ -73,8 +69,8 @@ msgid "5 min"
msgstr "5 دقائق" msgstr "5 دقائق"
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions" msgid "Actions"
msgstr "إجراءات" msgstr "إجراءات"
@@ -117,19 +113,18 @@ msgid "Agent"
msgstr "وكيل" msgstr "وكيل"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History" msgid "Alert History"
msgstr "سجل التنبيهات" msgstr "سجل التنبيهات"
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Alerts" msgid "Alerts"
msgstr "التنبيهات" msgstr "التنبيهات"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems" msgid "All Systems"
msgstr "جميع الأنظمة" msgstr "جميع الأنظمة"
@@ -153,8 +148,8 @@ msgstr "متوسط"
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "متوسط استخدام وحدة المعالجة المركزية للحاويات" msgstr "متوسط استخدام وحدة المعالجة المركزية للحاويات"
#. placeholder {0}: alertData.unit #. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "المتوسط يتجاوز <0>{value}{0}</0>" msgstr "المتوسط يتجاوز <0>{value}{0}</0>"
@@ -171,20 +166,16 @@ msgstr "متوسط استخدام وحدة المعالجة المركزية ع
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "متوسط ​​استخدام {0}" msgstr "متوسط ​​استخدام {0}"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups" msgid "Backups"
msgstr "النسخ الاحتياطية" msgstr "النسخ الاحتياطية"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "عرض النطاق الترددي" msgstr "عرض النطاق الترددي"
#: src/components/routes/system.tsx
msgid "Battery"
msgstr "البطارية"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers." msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2." msgstr "يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2."
@@ -211,8 +202,8 @@ msgstr "بايت (كيلوبايت/ثانية، ميجابايت/ثانية، ج
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "ذاكرة التخزين المؤقت / المخازن المؤقتة" msgstr "ذاكرة التخزين المؤقت / المخازن المؤقتة"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Cancel" msgid "Cancel"
msgstr "إلغاء" msgstr "إلغاء"
@@ -232,15 +223,6 @@ msgstr "تغيير وحدات عرض المقاييس."
msgid "Change general application options." msgid "Change general application options."
msgstr "تغيير خيارات التطبيق العامة." msgstr "تغيير خيارات التطبيق العامة."
#: src/components/routes/system.tsx
msgid "Charge"
msgstr "الشحن"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Charging"
msgstr "قيد الشحن"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
msgstr "خيارات الرسم البياني" msgstr "خيارات الرسم البياني"
@@ -279,8 +261,8 @@ msgstr "تأكيد كلمة المرور"
msgid "Connection is down" msgid "Connection is down"
msgstr "الاتصال مقطوع" msgstr "الاتصال مقطوع"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Continue" msgid "Continue"
msgstr "متابعة" msgstr "متابعة"
@@ -338,9 +320,9 @@ msgstr "نسخ YAML"
msgid "CPU" msgid "CPU"
msgstr "المعالج" msgstr "المعالج"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
msgstr "استخدام وحدة المعالجة المركزية" msgstr "استخدام وحدة المعالجة المركزية"
@@ -357,10 +339,10 @@ msgstr "أنشئت"
msgid "Critical (%)" msgid "Critical (%)"
msgstr "حرج (%)" msgstr "حرج (%)"
#. Context: Battery state #. Dark theme
#: src/components/routes/system.tsx #: src/components/mode-toggle.tsx
msgid "Current state" msgid "Dark"
msgstr "الحالة الحالية" msgstr "داكن"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -371,8 +353,8 @@ msgstr "لوحة التحكم"
msgid "Default time period" msgid "Default time period"
msgstr "الفترة الزمنية الافتراضية" msgstr "الفترة الزمنية الافتراضية"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Delete" msgid "Delete"
msgstr "حذف" msgstr "حذف"
@@ -380,11 +362,6 @@ msgstr "حذف"
msgid "Delete fingerprint" msgid "Delete fingerprint"
msgstr "حذف البصمة" msgstr "حذف البصمة"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
msgstr "قيد التفريغ"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "القرص" msgstr "القرص"
@@ -397,9 +374,9 @@ msgstr "إدخال/إخراج القرص"
msgid "Disk unit" msgid "Disk unit"
msgstr "وحدة القرص" msgstr "وحدة القرص"
#: src/components/charts/disk-chart.tsx #: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/components/charts/disk-chart.tsx
msgid "Disk Usage" msgid "Disk Usage"
msgstr "استخدام القرص" msgstr "استخدام القرص"
@@ -424,11 +401,10 @@ msgid "Documentation"
msgstr "التوثيق" msgstr "التوثيق"
#. Context: System is down #. Context: System is down
#: src/lib/utils.ts
#: src/components/alerts-history-columns.tsx #: 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-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Down" msgid "Down"
msgstr "معطل" msgstr "معطل"
@@ -441,8 +417,8 @@ msgstr "المدة"
msgid "Edit" msgid "Edit"
msgstr "تعديل" msgstr "تعديل"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email" msgid "Email"
msgstr "البريد الإشباكي" msgstr "البريد الإشباكي"
@@ -450,11 +426,6 @@ msgstr "البريد الإشباكي"
msgid "Email notifications" msgid "Email notifications"
msgstr "إشعارات البريد الإشباكي" msgstr "إشعارات البريد الإشباكي"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "فارغة"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيين كلمة المرور" msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيين كلمة المرور"
@@ -463,11 +434,11 @@ msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيي
msgid "Enter email address..." msgid "Enter email address..."
msgstr "أدخل عنوان البريد الإشباكي..." msgstr "أدخل عنوان البريد الإشباكي..."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/login/auth-form.tsx
msgid "Error" msgid "Error"
msgstr "خطأ" msgstr "خطأ"
@@ -498,12 +469,12 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "فهرنهايت (°ف)" msgstr "فهرنهايت (°ف)"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "فشل في المصادقة" msgstr "فشل في المصادقة"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings" msgid "Failed to save settings"
msgstr "فشل في حفظ الإعدادات" msgstr "فشل في حفظ الإعدادات"
@@ -511,13 +482,13 @@ msgstr "فشل في حفظ الإعدادات"
msgid "Failed to send test notification" msgid "Failed to send test notification"
msgstr "فشل في إرسال إشعار الاختبار" msgstr "فشل في إرسال إشعار الاختبار"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert" msgid "Failed to update alert"
msgstr "فشل في تحديث التنبيه" msgstr "فشل في تحديث التنبيه"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "تصفية..." msgstr "تصفية..."
@@ -525,7 +496,7 @@ msgstr "تصفية..."
msgid "Fingerprint" msgid "Fingerprint"
msgstr "البصمة" msgstr "البصمة"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}" msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}"
@@ -533,14 +504,9 @@ msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}
msgid "Forgot password?" msgid "Forgot password?"
msgstr "هل نسيت كلمة المرور؟" msgstr "هل نسيت كلمة المرور؟"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "ممتلئة"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General" msgid "General"
msgstr "عام" msgstr "عام"
@@ -562,11 +528,6 @@ msgstr "أمر Homebrew"
msgid "Host / IP" msgid "Host / IP"
msgstr "مضيف / IP" msgstr "مضيف / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "خاملة"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command." msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "إذا فقدت كلمة المرور لحساب المسؤول الخاص بك، يمكنك إعادة تعيينها باستخدام الأمر التالي." msgstr "إذا فقدت كلمة المرور لحساب المسؤول الخاص بك، يمكنك إعادة تعيينها باستخدام الأمر التالي."
@@ -588,19 +549,24 @@ msgstr "اللغة"
msgid "Layout" msgid "Layout"
msgstr "التخطيط" msgstr "التخطيط"
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr "فاتح"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "متوسط التحميل" msgstr "متوسط التحميل"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "متوسط التحميل 15 دقيقة" msgstr "متوسط التحميل 15 دقيقة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "متوسط التحميل 1 دقيقة" msgstr "متوسط التحميل 1 دقيقة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "متوسط التحميل 5 دقائق" msgstr "متوسط التحميل 5 دقائق"
@@ -617,13 +583,13 @@ msgstr "تسجيل الخروج"
msgid "Login" msgid "Login"
msgstr "تسجيل الدخول" msgstr "تسجيل الدخول"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed" msgid "Login attempt failed"
msgstr "فشل محاولة تسجيل الدخول" msgstr "فشل محاولة تسجيل الدخول"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs" msgid "Logs"
msgstr "السجلات" msgstr "السجلات"
@@ -648,8 +614,8 @@ msgstr "الحد الأقصى دقيقة"
msgid "Memory" msgid "Memory"
msgstr "الذاكرة" msgstr "الذاكرة"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "استخدام الذاكرة" msgstr "استخدام الذاكرة"
@@ -657,8 +623,8 @@ msgstr "استخدام الذاكرة"
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "استخدام الذاكرة لحاويات دوكر" msgstr "استخدام الذاكرة لحاويات دوكر"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
msgid "Name" msgid "Name"
msgstr "الاسم" msgstr "الاسم"
@@ -693,8 +659,8 @@ msgid "No systems found."
msgstr "لم يتم العثور على أنظمة." msgstr "لم يتم العثور على أنظمة."
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications" msgid "Notifications"
msgstr "الإشعارات" msgstr "الإشعارات"
@@ -706,9 +672,9 @@ msgstr "دعم OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file." msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف." msgstr "في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف."
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu" msgid "Open menu"
msgstr "فتح القائمة" msgstr "فتح القائمة"
@@ -716,7 +682,7 @@ msgstr "فتح القائمة"
msgid "Or continue with" msgid "Or continue with"
msgstr "أو المتابعة باستخدام" msgstr "أو المتابعة باستخدام"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts" msgid "Overwrite existing alerts"
msgstr "الكتابة فوق التنبيهات الحالية" msgstr "الكتابة فوق التنبيهات الحالية"
@@ -756,7 +722,6 @@ msgid "Pause"
msgstr "إيقاف مؤقت" msgstr "إيقاف مؤقت"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused" msgid "Paused"
msgstr "متوقف مؤقتا" msgstr "متوقف مؤقتا"
@@ -764,12 +729,12 @@ msgstr "متوقف مؤقتا"
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات." msgstr "يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "يرجى التحقق من السجلات لمزيد من التفاصيل." msgstr "يرجى التحقق من السجلات لمزيد من التفاصيل."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "يرجى التحقق من بيانات الاعتماد الخاصة بك والمحاولة مرة أخرى" msgstr "يرجى التحقق من بيانات الاعتماد الخاصة بك والمحاولة مرة أخرى"
@@ -781,7 +746,7 @@ msgstr "يرجى إنشاء حساب مسؤول"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع" msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "يرجى تسجيل الدخول مرة أخرى" msgstr "يرجى تسجيل الدخول مرة أخرى"
@@ -847,8 +812,8 @@ msgstr "صفوف لكل صفحة"
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي." msgstr "احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي."
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "حفظ الإعدادات" msgstr "حفظ الإعدادات"
@@ -864,7 +829,7 @@ msgstr "بحث"
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "البحث عن الأنظمة أو الإعدادات..." msgstr "البحث عن الأنظمة أو الإعدادات..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفية تلقي التنبيهات." msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفية تلقي التنبيهات."
@@ -908,8 +873,7 @@ msgstr "الترتيب حسب"
msgid "State" msgid "State"
msgstr "الحالة" msgstr "الحالة"
#: src/components/systems-table/systems-table.tsx #: src/lib/utils.ts
#: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "الحالة" msgstr "الحالة"
@@ -921,10 +885,12 @@ msgstr "مساحة التبديل المستخدمة من قبل النظام"
msgid "Swap Usage" msgid "Swap Usage"
msgstr "استخدام التبديل" msgstr "استخدام التبديل"
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System" msgid "System"
msgstr "النظام" msgstr "النظام"
@@ -949,8 +915,8 @@ msgstr "جدول"
msgid "Temp" msgid "Temp"
msgstr "درجة الحرارة" msgstr "درجة الحرارة"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "درجة الحرارة" msgstr "درجة الحرارة"
@@ -1009,8 +975,8 @@ msgid "Token"
msgstr "رمز مميز" msgstr "رمز مميز"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "الرموز المميزة والبصمات" msgstr "الرموز المميزة والبصمات"
@@ -1022,39 +988,39 @@ msgstr "تسمح الرموز المميزة للوكلاء بالاتصال و
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور." msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة دقيقة واحدة عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة دقيقة واحدة عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 15 دقيقة عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 15 دقيقة عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 5 دقائق عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 5 دقائق عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
msgstr "يتم التفعيل عندما <EFBFBD><EFBFBD>تجاوز أي مستشعر عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز الجمع بين الصعود/الهبوط عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز الجمع بين الصعود/الهبوط عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز استخدام وحدة المعالجة المركزية عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز استخدام وحدة المعالجة المركزية عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold" msgid "Triggers when memory usage exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز استخدام الذاكرة عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز استخدام الذاكرة عتبة معينة"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "يتم التفعيل عندما يتغير الحالة بين التشغيل والإيقاف" msgstr "يتم التفعيل عندما يتغير الحالة بين التشغيل والإيقاف"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة" msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة"
@@ -1067,15 +1033,9 @@ msgstr "تفضيلات الوحدة"
msgid "Universal token" msgid "Universal token"
msgstr "رمز مميز عالمي" msgstr "رمز مميز عالمي"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Unknown"
msgstr "غير معروفة"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
msgid "Up" msgid "Up"
msgstr "قيد التشغيل" msgstr "قيد التشغيل"
@@ -1098,13 +1058,13 @@ msgstr "الاستخدام"
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "استخدام القسم الجذر" msgstr "استخدام القسم الجذر"
#: src/components/charts/mem-chart.tsx
#: src/components/charts/swap-chart.tsx #: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
msgid "Used" msgid "Used"
msgstr "مستخدم" msgstr "مستخدم"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users" msgid "Users"
msgstr "المستخدمون" msgstr "المستخدمون"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n" "Language: bg\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-25 01:16\n" "PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -27,16 +27,12 @@ msgstr "{0, plural, one {# ден} other {# дни}}"
#. placeholder {1}: table.getFilteredRowModel().rows.length #. placeholder {1}: table.getFilteredRowModel().rows.length
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "{0} of {1} row(s) selected." msgid "{0} of {1} row(s) selected."
msgstr "{0} от {1} селектирани." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}" msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часа}}" msgstr "{hours, plural, one {# час} other {# часа}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 час" msgstr "1 час"
@@ -44,7 +40,7 @@ msgstr "1 час"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/charts/load-average-chart.tsx
msgid "1 min" msgid "1 min"
msgstr "1 минута" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 week" msgid "1 week"
@@ -57,7 +53,7 @@ msgstr "12 часа"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/charts/load-average-chart.tsx
msgid "15 min" msgid "15 min"
msgstr "15 минути" msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -70,18 +66,18 @@ msgstr "30 дни"
#. Load average #. Load average
#: src/components/charts/load-average-chart.tsx #: src/components/charts/load-average-chart.tsx
msgid "5 min" msgid "5 min"
msgstr "5 минути" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions" msgid "Actions"
msgstr "Действия" msgstr "Действия"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Active" msgid "Active"
msgstr "Активен" msgstr ""
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
msgid "Active Alerts" msgid "Active Alerts"
@@ -117,19 +113,18 @@ msgid "Agent"
msgstr "Агент" msgstr "Агент"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History" msgid "Alert History"
msgstr "История на нотификациите" msgstr ""
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Alerts" msgid "Alerts"
msgstr "Тревоги" msgstr "Тревоги"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems" msgid "All Systems"
msgstr "Всички системи" msgstr "Всички системи"
@@ -139,7 +134,7 @@ msgstr "Сигурен ли си, че искаш да изтриеш {name}?"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Are you sure?" msgid "Are you sure?"
msgstr "Сигурни ли сте?" msgstr ""
#: src/components/copy-to-clipboard.tsx #: src/components/copy-to-clipboard.tsx
msgid "Automatic copy requires a secure context." msgid "Automatic copy requires a secure context."
@@ -153,8 +148,8 @@ msgstr "Средно"
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Средно използване на процесора на контейнерите" msgstr "Средно използване на процесора на контейнерите"
#. placeholder {0}: alertData.unit #. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Средната стойност надхвърля <0>{value}{0}</0>" msgstr "Средната стойност надхвърля <0>{value}{0}</0>"
@@ -171,20 +166,16 @@ msgstr "Средно използване на процесора на цяла
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Средно използване на {0}" msgstr "Средно използване на {0}"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups" msgid "Backups"
msgstr "Архиви" msgstr "Архиви"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bandwidth на мрежата" msgstr "Bandwidth на мрежата"
#: src/components/routes/system.tsx
msgid "Battery"
msgstr "Батерия"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers." msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "Beszel поддържа OpenID Connect и много други OAuth2 доставчици за удостоверяване." msgstr "Beszel поддържа OpenID Connect и много други OAuth2 доставчици за удостоверяване."
@@ -200,19 +191,19 @@ msgstr "Двоичен код"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bits (Kbps, Mbps, Gbps)" msgid "Bits (Kbps, Mbps, Gbps)"
msgstr "Бита (Kbps, Mbps, Gbps)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Bytes (KB/s, MB/s, GB/s)" msgid "Bytes (KB/s, MB/s, GB/s)"
msgstr "Байта (KB/s, MB/s, GB/s)" msgstr ""
#: src/components/charts/mem-chart.tsx #: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Кеш / Буфери" msgstr "Кеш / Буфери"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Cancel" msgid "Cancel"
msgstr "Откажи" msgstr "Откажи"
@@ -222,25 +213,16 @@ msgstr "Внимание - възможност за загуба на данн
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "Целзий (°C)" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
msgstr "Промяна на единиците за показване на метриките." msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change general application options." msgid "Change general application options."
msgstr "Смени общите опции на приложението." msgstr "Смени общите опции на приложението."
#: src/components/routes/system.tsx
msgid "Charge"
msgstr "Заряд"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Charging"
msgstr "Зареждане"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
msgstr "Опции на диаграмата" msgstr "Опции на диаграмата"
@@ -277,10 +259,10 @@ msgstr "Потвърди парола"
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
msgid "Connection is down" msgid "Connection is down"
msgstr "Връзката е прекъсната" msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Continue" msgid "Continue"
msgstr "Продължи" msgstr "Продължи"
@@ -303,7 +285,7 @@ msgstr "Копирай docker run"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Environment variables" msgctxt "Environment variables"
msgid "Copy env" msgid "Copy env"
msgstr "Копирай еnv" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Copy host" msgid "Copy host"
@@ -324,23 +306,23 @@ msgstr "Копирай текста"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>." msgid "Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>."
msgstr "Копирайте командата за инсталиране на агента по-долу или регистрирайте агентите автоматично с <0>универсален токен</0>." msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>." msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
msgstr "Копирайте съдържанието на<0>docker-compose.yml</0> за агента по-долу или регистрирайте агентите автоматично с <1>универсален токен</1>." msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Copy YAML" msgid "Copy YAML"
msgstr "Копирай YAML" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "CPU" msgid "CPU"
msgstr "Процесор" msgstr "Процесор"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
msgstr "Употреба на процесор" msgstr "Употреба на процесор"
@@ -351,16 +333,16 @@ msgstr "Създай акаунт"
#. Context: date created #. Context: date created
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Created" msgid "Created"
msgstr "Създаден" msgstr ""
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Critical (%)" msgid "Critical (%)"
msgstr "Критично (%)" msgstr "Критично (%)"
#. Context: Battery state #. Dark theme
#: src/components/routes/system.tsx #: src/components/mode-toggle.tsx
msgid "Current state" msgid "Dark"
msgstr "Текущо състояние" msgstr "Тъмно"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -371,19 +353,14 @@ msgstr "Табло"
msgid "Default time period" msgid "Default time period"
msgstr "Времеви диапазон по подразбиране" msgstr "Времеви диапазон по подразбиране"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Delete" msgid "Delete"
msgstr "Изтрий" msgstr "Изтрий"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Delete fingerprint" msgid "Delete fingerprint"
msgstr "Изтрий пръстов отпечатък" msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
msgstr "Разреждане"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
@@ -395,11 +372,11 @@ msgstr "Диск I/O"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "Единица за диск" msgstr ""
#: src/components/charts/disk-chart.tsx #: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/components/charts/disk-chart.tsx
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Използване на диск" msgstr "Използване на диск"
@@ -424,25 +401,24 @@ msgid "Documentation"
msgstr "Документация" msgstr "Документация"
#. Context: System is down #. Context: System is down
#: src/lib/utils.ts
#: src/components/alerts-history-columns.tsx #: 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-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Down" msgid "Down"
msgstr "Офлайн" msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Продължителност" msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Edit" msgid "Edit"
msgstr "Редактирай" msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email" msgid "Email"
msgstr "Имейл" msgstr "Имейл"
@@ -450,11 +426,6 @@ msgstr "Имейл"
msgid "Email notifications" msgid "Email notifications"
msgstr "Имейл нотификации" msgstr "Имейл нотификации"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "Празна"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
msgstr "Въведи имейл адрес за да нулираш паролата" msgstr "Въведи имейл адрес за да нулираш паролата"
@@ -463,11 +434,11 @@ msgstr "Въведи имейл адрес за да нулираш парола
msgid "Enter email address..." msgid "Enter email address..."
msgstr "Въведи имейл адрес..." msgstr "Въведи имейл адрес..."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/login/auth-form.tsx
msgid "Error" msgid "Error"
msgstr "Грешка" msgstr "Грешка"
@@ -484,7 +455,7 @@ msgstr "Съществуващи системи които не са дефин
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Export" msgid "Export"
msgstr "Експортиране" msgstr ""
#: src/components/routes/settings/config-yaml.tsx #: src/components/routes/settings/config-yaml.tsx
msgid "Export configuration" msgid "Export configuration"
@@ -496,14 +467,14 @@ msgstr "Експортирай конфигурацията на системи
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Фаренхайт (°F)" msgstr ""
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Неуспешно удостоверяване" msgstr "Неуспешно удостоверяване"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings" msgid "Failed to save settings"
msgstr "Неуспешно запазване на настройки" msgstr "Неуспешно запазване на настройки"
@@ -511,21 +482,21 @@ msgstr "Неуспешно запазване на настройки"
msgid "Failed to send test notification" msgid "Failed to send test notification"
msgstr "Неуспешно изпрати тестова нотификация" msgstr "Неуспешно изпрати тестова нотификация"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert" msgid "Failed to update alert"
msgstr "Неуспешно обнови тревога" msgstr "Неуспешно обнови тревога"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Филтрирай..." msgstr "Филтрирай..."
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint" msgid "Fingerprint"
msgstr "Пръстов отпечатък" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}" msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}"
@@ -533,14 +504,9 @@ msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}
msgid "Forgot password?" msgid "Forgot password?"
msgstr "Забравена парола?" msgstr "Забравена парола?"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Пълна"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General" msgid "General"
msgstr "Общо" msgstr "Общо"
@@ -562,11 +528,6 @@ msgstr "Команда Homebrew"
msgid "Host / IP" msgid "Host / IP"
msgstr "Хост / IP" msgstr "Хост / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "Неактивна"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command." msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Ако си загубил паролата до администраторския акаунт, можеш да я нулираш със следващата команда." msgstr "Ако си загубил паролата до администраторския акаунт, можеш да я нулираш със следващата команда."
@@ -588,26 +549,31 @@ msgstr "Език"
msgid "Layout" msgid "Layout"
msgstr "Подреждане" msgstr "Подреждане"
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr "Светъл"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Средно натоварване" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "Средно натоварване 15 минути" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "Средно натоварване 1 минута" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "Средно натоварване 5 минути" msgstr ""
#. Short label for load average #. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg" msgid "Load Avg"
msgstr "Средно натоварване" msgstr ""
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
@@ -617,13 +583,13 @@ msgstr "Изход"
msgid "Login" msgid "Login"
msgstr "Вход" msgstr "Вход"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed" msgid "Login attempt failed"
msgstr "Неуспешен опит за вход" msgstr "Неуспешен опит за вход"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs" msgid "Logs"
msgstr "Логове" msgstr "Логове"
@@ -637,7 +603,7 @@ msgstr "Управление на предпочитанията за показ
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Manual setup instructions" msgid "Manual setup instructions"
msgstr "Инструкции за ръчна настройка" msgstr ""
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -648,8 +614,8 @@ msgstr "Максимум 1 минута"
msgid "Memory" msgid "Memory"
msgstr "Памет" msgstr "Памет"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Употреба на паметта" msgstr "Употреба на паметта"
@@ -657,8 +623,8 @@ msgstr "Употреба на паметта"
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Използването на памет от docker контейнерите" msgstr "Използването на памет от docker контейнерите"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
msgid "Name" msgid "Name"
msgstr "Име" msgstr "Име"
@@ -677,7 +643,7 @@ msgstr "Мрежов трафик на публични интерфейси"
#. Context: Bytes or bits #. Context: Bytes or bits
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Network unit" msgid "Network unit"
msgstr "Единица за измерване на скорост" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "No results found." msgid "No results found."
@@ -685,7 +651,7 @@ msgstr "Няма намерени резултати."
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "No results." msgid "No results."
msgstr "Няма резултати." msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
@@ -693,8 +659,8 @@ msgid "No systems found."
msgstr "Няма намерени системи." msgstr "Няма намерени системи."
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications" msgid "Notifications"
msgstr "Нотификации" msgstr "Нотификации"
@@ -706,9 +672,9 @@ msgstr "Поддръжка на OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file." msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла." msgstr "На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла."
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu" msgid "Open menu"
msgstr "Отвори менюто" msgstr "Отвори менюто"
@@ -716,7 +682,7 @@ msgstr "Отвори менюто"
msgid "Or continue with" msgid "Or continue with"
msgstr "Или продължи с" msgstr "Или продължи с"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts" msgid "Overwrite existing alerts"
msgstr "Презапиши съществуващи тревоги" msgstr "Презапиши съществуващи тревоги"
@@ -728,7 +694,7 @@ msgstr "Страница"
#. placeholder {1}: table.getPageCount() #. placeholder {1}: table.getPageCount()
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Page {0} of {1}" msgid "Page {0} of {1}"
msgstr "Страница {0} от {1}" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "Pages / Settings" msgid "Pages / Settings"
@@ -745,7 +711,7 @@ msgstr "Паролата трябва да е поне 8 символа."
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Password must be less than 72 bytes." msgid "Password must be less than 72 bytes."
msgstr "Паролата трябва да е по-малка от 72 байта." msgstr ""
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Password reset request received" msgid "Password reset request received"
@@ -756,7 +722,6 @@ msgid "Pause"
msgstr "Пауза" msgstr "Пауза"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused" msgid "Paused"
msgstr "На пауза" msgstr "На пауза"
@@ -764,12 +729,12 @@ msgstr "На пауза"
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени." msgstr "Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "Моля провери log-овете за повече информация." msgstr "Моля провери log-овете за повече информация."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "Моля провери дадената информация и опитай отново" msgstr "Моля провери дадената информация и опитай отново"
@@ -781,7 +746,7 @@ msgstr "Моля създай администраторски акаунт"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Моля активирай изскачащите прозорци за този сайт" msgstr "Моля активирай изскачащите прозорци за този сайт"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Моля влез отново" msgstr "Моля влез отново"
@@ -829,7 +794,7 @@ msgstr "Нулиране на парола"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Resolved" msgid "Resolved"
msgstr "Решен" msgstr ""
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Resume" msgid "Resume"
@@ -837,24 +802,24 @@ msgstr "Възобнови"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token" msgid "Rotate token"
msgstr "Пресъздаване на идентификатора" msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Rows per page" msgid "Rows per page"
msgstr "Редове на страница" msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл." msgstr "Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл."
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "Запази настройките" msgstr "Запази настройките"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Save system" msgid "Save system"
msgstr "Запази система" msgstr ""
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Search" msgid "Search"
@@ -864,7 +829,7 @@ msgstr "Търси"
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "Търси за системи или настройки..." msgstr "Търси за системи или настройки..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Виж <0>настройките за нотификациите</0> за да конфигурираш как получаваш тревоги." msgstr "Виж <0>настройките за нотификациите</0> за да конфигурираш как получаваш тревоги."
@@ -906,10 +871,9 @@ msgstr "Сортиране по"
#. Context: alert state (active or resolved) #. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "State" msgid "State"
msgstr "Състояние" msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/lib/utils.ts
#: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Статус" msgstr "Статус"
@@ -921,16 +885,18 @@ msgstr "Изполван swap от системата"
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Използване на swap" msgstr "Използване на swap"
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System" msgid "System"
msgstr "Система" msgstr "Система"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "Средно натоварване на системата във времето" msgstr ""
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Systems" msgid "Systems"
@@ -947,16 +913,16 @@ msgstr "Таблица"
#. Temperature label in systems table #. Temperature label in systems table
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Temp" msgid "Temp"
msgstr "Температура" msgstr ""
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Температура" msgstr "Температура"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Temperature unit" msgid "Temperature unit"
msgstr "Единица за температура" msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
@@ -980,7 +946,7 @@ msgstr "Това действие не може да бъде отменено.
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "This will permanently delete all selected records from the database." msgid "This will permanently delete all selected records from the database."
msgstr "Това ще доведе до трайно изтриване на всички избрани записи от базата данни." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
@@ -1006,78 +972,72 @@ msgstr "Включи тема"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "Токен" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "Токен & Пръстов отпечатък" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection." msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
msgstr "Токените позволяват на агентите да се свързват и регистрират. Отпечатъците са стабилни идентификатори, уникални за всяка система, които се задават при първото свързване." msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора." msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта за 1 минута надвиши зададен праг" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта за 15 минута надвиши зададен праг" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта за 5 минута надвиши зададен праг" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
msgstr "Задейства се, когато някой даден сензор надвиши зададен праг" msgstr "Задейства се, когато някой даден сензор надвиши зададен праг"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Задейства се, когато комбинираното качване/сваляне надвиши зададен праг" msgstr "Задейства се, когато комбинираното качване/сваляне надвиши зададен праг"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Задейства се, когато употребата на процесора надвиши зададен праг" msgstr "Задейства се, когато употребата на процесора надвиши зададен праг"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold" msgid "Triggers when memory usage exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта надвиши зададен праг" msgstr "Задейства се, когато употребата на паметта надвиши зададен праг"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "Задейства се, когато статуса превключва между долу и горе" msgstr "Задейства се, когато статуса превключва между долу и горе"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Задейства се, когато употребата на някой диск надивши зададен праг" msgstr "Задейства се, когато употребата на някой диск надивши зададен праг"
#. Temperature / network units #. Temperature / network units
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Предпочитания на единицата" msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Универсален тоукън" msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Unknown"
msgstr "Неизвестна"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
msgid "Up" msgid "Up"
msgstr "Нагоре" msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Updated in real time. Click on a system to view information." msgid "Updated in real time. Click on a system to view information."
@@ -1098,19 +1058,19 @@ msgstr "Употреба"
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Употреба на root partition-а" msgstr "Употреба на root partition-а"
#: src/components/charts/mem-chart.tsx
#: src/components/charts/swap-chart.tsx #: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
msgid "Used" msgid "Used"
msgstr "Използвани" msgstr "Използвани"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users" msgid "Users"
msgstr "Потребители" msgstr "Потребители"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "Стойност" msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "View" msgid "View"
@@ -1118,7 +1078,7 @@ msgstr "Изглед"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts." msgid "View your 200 most recent alerts."
msgstr "Прегледайте последните си 200 сигнала." msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Visible Fields" msgid "Visible Fields"
@@ -1146,7 +1106,7 @@ msgstr "Webhook / Пуш нотификации"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart." msgid "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
msgstr "Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система. Изтича след един час или при рестартиране на хъба." msgstr ""
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n" "Language: cs\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-25 01:16\n" "PO-Revision-Date: 2025-08-04 01:51\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n" "Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -33,10 +33,6 @@ msgstr "{0} z {1} vybraných řádků."
msgid "{hours, plural, one {# hour} other {# hours}}" msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}" 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 #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 hodina" msgstr "1 hodina"
@@ -73,8 +69,8 @@ msgid "5 min"
msgstr "5 min" msgstr "5 min"
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions" msgid "Actions"
msgstr "Akce" msgstr "Akce"
@@ -117,19 +113,18 @@ msgid "Agent"
msgstr "Agent" msgstr "Agent"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History" msgid "Alert History"
msgstr "Historie upozornění" msgstr "Historie upozornění"
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Alerts" msgid "Alerts"
msgstr "Výstrahy" msgstr "Výstrahy"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems" msgid "All Systems"
msgstr "Všechny systémy" msgstr "Všechny systémy"
@@ -153,8 +148,8 @@ msgstr "Průměr"
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Průměrné využití CPU kontejnerů" msgstr "Průměrné využití CPU kontejnerů"
#. placeholder {0}: alertData.unit #. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Průměr je vyšší než <0>{value}{0}</0>" msgstr "Průměr je vyšší než <0>{value}{0}</0>"
@@ -171,20 +166,16 @@ msgstr "Průměrné využití CPU v celém systému"
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Průměrné využití {0}" msgstr "Průměrné využití {0}"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups" msgid "Backups"
msgstr "Zálohy" msgstr "Zálohy"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Přenos" msgstr "Přenos"
#: src/components/routes/system.tsx
msgid "Battery"
msgstr "Baterie"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers." msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "Beszel podporuje OpenID Connect a mnoho poskytovatelů OAuth2 ověřování." msgstr "Beszel podporuje OpenID Connect a mnoho poskytovatelů OAuth2 ověřování."
@@ -211,8 +202,8 @@ msgstr "Bytes (KB/s, MB/s, GB/s)"
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / vyrovnávací paměť" msgstr "Cache / vyrovnávací paměť"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Cancel" msgid "Cancel"
msgstr "Zrušit" msgstr "Zrušit"
@@ -232,15 +223,6 @@ msgstr "Změnit jednotky zobrazení metrik."
msgid "Change general application options." msgid "Change general application options."
msgstr "Změnit obecné nastavení aplikace." msgstr "Změnit obecné nastavení aplikace."
#: src/components/routes/system.tsx
msgid "Charge"
msgstr "Nabíjení"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Charging"
msgstr "Nabíjení"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
msgstr "Možnosti grafu" msgstr "Možnosti grafu"
@@ -279,8 +261,8 @@ msgstr "Potvrdit heslo"
msgid "Connection is down" msgid "Connection is down"
msgstr "Připojení je nedostupné" msgstr "Připojení je nedostupné"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Continue" msgid "Continue"
msgstr "Pokračovat" msgstr "Pokračovat"
@@ -338,9 +320,9 @@ msgstr "Kopírovat YAML"
msgid "CPU" msgid "CPU"
msgstr "Procesor" msgstr "Procesor"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
msgstr "Využití procesoru" msgstr "Využití procesoru"
@@ -357,10 +339,10 @@ msgstr "Vytvořeno"
msgid "Critical (%)" msgid "Critical (%)"
msgstr "Kritické (%)" msgstr "Kritické (%)"
#. Context: Battery state #. Dark theme
#: src/components/routes/system.tsx #: src/components/mode-toggle.tsx
msgid "Current state" msgid "Dark"
msgstr "Aktuální stav" msgstr "Tmavý"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -371,8 +353,8 @@ msgstr "Přehled"
msgid "Default time period" msgid "Default time period"
msgstr "Výchozí doba" msgstr "Výchozí doba"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Delete" msgid "Delete"
msgstr "Odstranit" msgstr "Odstranit"
@@ -380,11 +362,6 @@ msgstr "Odstranit"
msgid "Delete fingerprint" msgid "Delete fingerprint"
msgstr "Smazat identifikátor" msgstr "Smazat identifikátor"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
msgstr "Vybíjení"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr "Disk"
@@ -397,9 +374,9 @@ msgstr "Disk I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Disková jednotka" msgstr "Disková jednotka"
#: src/components/charts/disk-chart.tsx #: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/components/charts/disk-chart.tsx
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Využití disku" msgstr "Využití disku"
@@ -424,11 +401,10 @@ msgid "Documentation"
msgstr "Dokumentace" msgstr "Dokumentace"
#. Context: System is down #. Context: System is down
#: src/lib/utils.ts
#: src/components/alerts-history-columns.tsx #: 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-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Down" msgid "Down"
msgstr "Nefunkční" msgstr "Nefunkční"
@@ -441,8 +417,8 @@ msgstr "Doba trvání"
msgid "Edit" msgid "Edit"
msgstr "Upravit" msgstr "Upravit"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
@@ -450,11 +426,6 @@ msgstr "Email"
msgid "Email notifications" msgid "Email notifications"
msgstr "Emailová upozornění" msgstr "Emailová upozornění"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "Prázdná"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
msgstr "Zadejte e-mailovou adresu pro obnovu hesla" msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
@@ -463,11 +434,11 @@ msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
msgid "Enter email address..." msgid "Enter email address..."
msgstr "Zadejte e-mailovou adresu..." msgstr "Zadejte e-mailovou adresu..."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/login/auth-form.tsx
msgid "Error" msgid "Error"
msgstr "Chyba" msgstr "Chyba"
@@ -498,12 +469,12 @@ msgstr "Exportovat aktuální konfiguraci systémů."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheita (°F)" msgstr "Fahrenheita (°F)"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Ověření se nezdařilo" msgstr "Ověření se nezdařilo"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings" msgid "Failed to save settings"
msgstr "Nepodařilo se uložit nastavení" msgstr "Nepodařilo se uložit nastavení"
@@ -511,13 +482,13 @@ msgstr "Nepodařilo se uložit nastavení"
msgid "Failed to send test notification" msgid "Failed to send test notification"
msgstr "Nepodařilo se odeslat testovací oznámení" msgstr "Nepodařilo se odeslat testovací oznámení"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert" msgid "Failed to update alert"
msgstr "Nepodařilo se aktualizovat upozornění" msgstr "Nepodařilo se aktualizovat upozornění"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Filtr..." msgstr "Filtr..."
@@ -525,7 +496,7 @@ msgstr "Filtr..."
msgid "Fingerprint" msgid "Fingerprint"
msgstr "Otisk" msgstr "Otisk"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}" msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}"
@@ -533,14 +504,9 @@ msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}"
msgid "Forgot password?" msgid "Forgot password?"
msgstr "Zapomněli jste heslo?" msgstr "Zapomněli jste heslo?"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Plná"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General" msgid "General"
msgstr "Obecné" msgstr "Obecné"
@@ -562,11 +528,6 @@ msgstr "Homebrew příkaz"
msgid "Host / IP" msgid "Host / IP"
msgstr "Hostitel / IP" msgstr "Hostitel / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "Neaktivní"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command." msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Pokud jste ztratili heslo k vašemu účtu správce, můžete jej obnovit pomocí následujícího příkazu." msgstr "Pokud jste ztratili heslo k vašemu účtu správce, můžete jej obnovit pomocí následujícího příkazu."
@@ -588,19 +549,24 @@ msgstr "Jazyk"
msgid "Layout" msgid "Layout"
msgstr "Rozvržení" msgstr "Rozvržení"
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr "Světlý"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Průměrné vytížení" msgstr "Průměrné vytížení"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "Průměrná zátěž 15m" msgstr "Průměrná zátěž 15m"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "Průměrná zátěž 1m" msgstr "Průměrná zátěž 1m"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "Průměrná zátěž 5m" msgstr "Průměrná zátěž 5m"
@@ -617,13 +583,13 @@ msgstr "Odhlásit"
msgid "Login" msgid "Login"
msgstr "Přihlásit" msgstr "Přihlásit"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed" msgid "Login attempt failed"
msgstr "Pokus o přihlášení selhal" msgstr "Pokus o přihlášení selhal"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs" msgid "Logs"
msgstr "Logy" msgstr "Logy"
@@ -648,8 +614,8 @@ msgstr "Max. 1 min"
msgid "Memory" msgid "Memory"
msgstr "Paměť" msgstr "Paměť"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Využití paměti" msgstr "Využití paměti"
@@ -657,8 +623,8 @@ msgstr "Využití paměti"
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Využití paměti docker kontejnerů" msgstr "Využití paměti docker kontejnerů"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
msgid "Name" msgid "Name"
msgstr "Název" msgstr "Název"
@@ -693,8 +659,8 @@ msgid "No systems found."
msgstr "Nenalezeny žádné systémy." msgstr "Nenalezeny žádné systémy."
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications" msgid "Notifications"
msgstr "Upozornění" msgstr "Upozornění"
@@ -706,9 +672,9 @@ msgstr "Podpora OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file." msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru." msgstr "Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru."
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu" msgid "Open menu"
msgstr "Otevřít menu" msgstr "Otevřít menu"
@@ -716,7 +682,7 @@ msgstr "Otevřít menu"
msgid "Or continue with" msgid "Or continue with"
msgstr "Nebo pokračujte s" msgstr "Nebo pokračujte s"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts" msgid "Overwrite existing alerts"
msgstr "Přepsat existující upozornění" msgstr "Přepsat existující upozornění"
@@ -756,7 +722,6 @@ msgid "Pause"
msgstr "Pozastavit" msgstr "Pozastavit"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused" msgid "Paused"
msgstr "Pozastaveno" msgstr "Pozastaveno"
@@ -764,12 +729,12 @@ msgstr "Pozastaveno"
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "<0>nakonfigurujte SMTP server</0> pro zajištění toho, aby byla upozornění doručena." msgstr "<0>nakonfigurujte SMTP server</0> pro zajištění toho, aby byla upozornění doručena."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "Pro více informací zkontrolujte logy." msgstr "Pro více informací zkontrolujte logy."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "Zkontrolujte prosím Vaše přihlašovací údaje a zkuste to znovu" msgstr "Zkontrolujte prosím Vaše přihlašovací údaje a zkuste to znovu"
@@ -781,7 +746,7 @@ msgstr "Vytvořte si prosím účet administrátora"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Prosím povolte vyskakovací okna pro tento web" msgstr "Prosím povolte vyskakovací okna pro tento web"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Přihlaste se prosím znovu" msgstr "Přihlaste se prosím znovu"
@@ -847,8 +812,8 @@ msgstr "Řádků na stránku"
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole." msgstr "Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole."
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "Uložit nastavení" msgstr "Uložit nastavení"
@@ -864,7 +829,7 @@ msgstr "Hledat"
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "Hledat systémy nebo nastavení..." msgstr "Hledat systémy nebo nastavení..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak přijímáte upozornění." msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak přijímáte upozornění."
@@ -908,8 +873,7 @@ msgstr "Seřadit podle"
msgid "State" msgid "State"
msgstr "Stav" msgstr "Stav"
#: src/components/systems-table/systems-table.tsx #: src/lib/utils.ts
#: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Stav" msgstr "Stav"
@@ -921,10 +885,12 @@ msgstr "Swap prostor využívaný systémem"
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap využití" msgstr "Swap využití"
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System" msgid "System"
msgstr "Systém" msgstr "Systém"
@@ -949,8 +915,8 @@ msgstr "Tabulka"
msgid "Temp" msgid "Temp"
msgstr "Teplota" msgstr "Teplota"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Teplota" msgstr "Teplota"
@@ -1009,8 +975,8 @@ msgid "Token"
msgstr "Token" msgstr "Token"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "Tokeny & Otisky" msgstr "Tokeny & Otisky"
@@ -1022,39 +988,39 @@ msgstr "Tokeny umožňují agentům připojení a registraci. Otisky jsou stabil
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu." msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu."
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Spustí se, když využití paměti během 1 minuty překročí prahovou hodnotu" msgstr "Spustí se, když využití paměti během 1 minuty překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "Spustí se, když využití paměti během 15 minut překročí prahovou hodnotu" msgstr "Spustí se, když využití paměti během 15 minut překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Spustí se, když využití paměti během 5 minut překročí prahovou hodnotu" msgstr "Spustí se, když využití paměti během 5 minut překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
msgstr "Spustí se, když některý senzor překročí prahovou hodnotu" msgstr "Spustí se, když některý senzor překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Spustí se, když kombinace up/down překročí prahovou hodnotu" msgstr "Spustí se, když kombinace up/down překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Spustí se, když využití procesoru překročí prahovou hodnotu" msgstr "Spustí se, když využití procesoru překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold" msgid "Triggers when memory usage exceeds a threshold"
msgstr "Spustí se, když využití paměti překročí prahovou hodnotu" msgstr "Spustí se, když využití paměti překročí prahovou hodnotu"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "Spouští se, když se změní dostupnost" msgstr "Spouští se, když se změní dostupnost"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Spustí se, když využití disku překročí prahovou hodnotu" msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
@@ -1067,15 +1033,9 @@ msgstr "Předvolby jednotek"
msgid "Universal token" msgid "Universal token"
msgstr "Univerzální token" msgstr "Univerzální token"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Unknown"
msgstr "Neznámá"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
msgid "Up" msgid "Up"
msgstr "Funkční" msgstr "Funkční"
@@ -1098,13 +1058,13 @@ msgstr "Využití"
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Využití kořenového oddílu" msgstr "Využití kořenového oddílu"
#: src/components/charts/mem-chart.tsx
#: src/components/charts/swap-chart.tsx #: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
msgid "Used" msgid "Used"
msgstr "Využito" msgstr "Využito"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users" msgid "Users"
msgstr "Uživatelé" msgstr "Uživatelé"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n" "Language: da\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-25 01:16\n" "PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -33,10 +33,6 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}" msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{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 #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 time" msgstr "1 time"
@@ -73,8 +69,8 @@ msgid "5 min"
msgstr "" msgstr ""
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions" msgid "Actions"
msgstr "Handlinger" msgstr "Handlinger"
@@ -117,19 +113,18 @@ msgid "Agent"
msgstr "Agent" msgstr "Agent"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History" msgid "Alert History"
msgstr "" msgstr ""
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Alerts" msgid "Alerts"
msgstr "Alarmer" msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems" msgid "All Systems"
msgstr "Alle systemer" msgstr "Alle systemer"
@@ -153,8 +148,8 @@ msgstr "Gennemsnitlig"
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Gennemsnitlig CPU udnyttelse af containere" msgstr "Gennemsnitlig CPU udnyttelse af containere"
#. placeholder {0}: alertData.unit #. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Gennemsnit overstiger <0>{value}{0}</0>" msgstr "Gennemsnit overstiger <0>{value}{0}</0>"
@@ -171,20 +166,16 @@ msgstr "Gennemsnitlig systembaseret CPU-udnyttelse"
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Gennemsnitlig udnyttelse af {0}" msgstr "Gennemsnitlig udnyttelse af {0}"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups" msgid "Backups"
msgstr "Sikkerhedskopier" msgstr "Sikkerhedskopier"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Båndbredde" msgstr "Båndbredde"
#: src/components/routes/system.tsx
msgid "Battery"
msgstr "Batteri"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers." msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "Beszel understøtter OpenID Connect og mange OAuth2 godkendelsesudbydere." msgstr "Beszel understøtter OpenID Connect og mange OAuth2 godkendelsesudbydere."
@@ -211,8 +202,8 @@ msgstr ""
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Buffere" msgstr "Cache / Buffere"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Cancel" msgid "Cancel"
msgstr "Fortryd" msgstr "Fortryd"
@@ -232,15 +223,6 @@ msgstr ""
msgid "Change general application options." msgid "Change general application options."
msgstr "Skift generelle applikationsindstillinger." msgstr "Skift generelle applikationsindstillinger."
#: src/components/routes/system.tsx
msgid "Charge"
msgstr "Opladning"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Charging"
msgstr "Oplader"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
msgstr "Diagrammuligheder" msgstr "Diagrammuligheder"
@@ -279,8 +261,8 @@ msgstr "Bekræft adgangskode"
msgid "Connection is down" msgid "Connection is down"
msgstr "" msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Continue" msgid "Continue"
msgstr "Forsæt" msgstr "Forsæt"
@@ -338,9 +320,9 @@ msgstr ""
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr "CPU"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
msgstr "CPU forbrug" msgstr "CPU forbrug"
@@ -357,10 +339,10 @@ msgstr ""
msgid "Critical (%)" msgid "Critical (%)"
msgstr "Kritisk (%)" msgstr "Kritisk (%)"
#. Context: Battery state #. Dark theme
#: src/components/routes/system.tsx #: src/components/mode-toggle.tsx
msgid "Current state" msgid "Dark"
msgstr "Nuværende tilstand" msgstr "Mørk"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -371,8 +353,8 @@ msgstr "Oversigtspanel"
msgid "Default time period" msgid "Default time period"
msgstr "Standard tidsperiode" msgstr "Standard tidsperiode"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Delete" msgid "Delete"
msgstr "Slet" msgstr "Slet"
@@ -380,11 +362,6 @@ msgstr "Slet"
msgid "Delete fingerprint" msgid "Delete fingerprint"
msgstr "" msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
msgstr "Aflader"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Disk" msgstr "Disk"
@@ -397,9 +374,9 @@ msgstr "Disk I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "" msgstr ""
#: src/components/charts/disk-chart.tsx #: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/components/charts/disk-chart.tsx
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Diskforbrug" msgstr "Diskforbrug"
@@ -424,11 +401,10 @@ msgid "Documentation"
msgstr "Dokumentation" msgstr "Dokumentation"
#. Context: System is down #. Context: System is down
#: src/lib/utils.ts
#: src/components/alerts-history-columns.tsx #: 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-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Down" msgid "Down"
msgstr "Nede" msgstr "Nede"
@@ -441,8 +417,8 @@ msgstr ""
msgid "Edit" msgid "Edit"
msgstr "Rediger" msgstr "Rediger"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email" msgid "Email"
msgstr "E-mail" msgstr "E-mail"
@@ -450,11 +426,6 @@ msgstr "E-mail"
msgid "Email notifications" msgid "Email notifications"
msgstr "Email-notifikationer" msgstr "Email-notifikationer"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "Tom"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
msgstr "Indtast e-mailadresse for at nulstille adgangskoden" msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
@@ -463,11 +434,11 @@ msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
msgid "Enter email address..." msgid "Enter email address..."
msgstr "Indtast e-mailadresse..." msgstr "Indtast e-mailadresse..."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/login/auth-form.tsx
msgid "Error" msgid "Error"
msgstr "Fejl" msgstr "Fejl"
@@ -498,12 +469,12 @@ msgstr "Eksporter din nuværende systemkonfiguration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Kunne ikke godkende" msgstr "Kunne ikke godkende"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings" msgid "Failed to save settings"
msgstr "Kunne ikke gemme indstillinger" msgstr "Kunne ikke gemme indstillinger"
@@ -511,13 +482,13 @@ msgstr "Kunne ikke gemme indstillinger"
msgid "Failed to send test notification" msgid "Failed to send test notification"
msgstr "Afsendelse af testnotifikation mislykkedes" msgstr "Afsendelse af testnotifikation mislykkedes"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert" msgid "Failed to update alert"
msgstr "Kunne ikke opdatere alarm" msgstr "Kunne ikke opdatere alarm"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Filter..." msgstr "Filter..."
@@ -525,7 +496,7 @@ msgstr "Filter..."
msgid "Fingerprint" msgid "Fingerprint"
msgstr "" msgstr ""
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}" msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}"
@@ -533,14 +504,9 @@ msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}"
msgid "Forgot password?" msgid "Forgot password?"
msgstr "Glemt adgangskode?" msgstr "Glemt adgangskode?"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Fuldt opladt"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General" msgid "General"
msgstr "Generelt" msgstr "Generelt"
@@ -562,11 +528,6 @@ msgstr "Homebrew-kommando"
msgid "Host / IP" msgid "Host / IP"
msgstr "Vært / IP" msgstr "Vært / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "Inaktiv"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command." msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Hvis du har mistet adgangskoden til din administratorkonto, kan du nulstille den ved hjælp af følgende kommando." msgstr "Hvis du har mistet adgangskoden til din administratorkonto, kan du nulstille den ved hjælp af følgende kommando."
@@ -588,19 +549,24 @@ msgstr "Sprog"
msgid "Layout" msgid "Layout"
msgstr "Layout" msgstr "Layout"
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr "Lys"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "" msgstr ""
@@ -617,13 +583,13 @@ msgstr "Log ud"
msgid "Login" msgid "Login"
msgstr "Log ind" msgstr "Log ind"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed" msgid "Login attempt failed"
msgstr "Loginforsøg mislykkedes" msgstr "Loginforsøg mislykkedes"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs" msgid "Logs"
msgstr "Logs" msgstr "Logs"
@@ -648,8 +614,8 @@ msgstr "Maks. 1 min"
msgid "Memory" msgid "Memory"
msgstr "Hukommelse" msgstr "Hukommelse"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Hukommelsesforbrug" msgstr "Hukommelsesforbrug"
@@ -657,8 +623,8 @@ msgstr "Hukommelsesforbrug"
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Hukommelsesforbrug af dockercontainere" msgstr "Hukommelsesforbrug af dockercontainere"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
msgid "Name" msgid "Name"
msgstr "Navn" msgstr "Navn"
@@ -693,8 +659,8 @@ msgid "No systems found."
msgstr "Ingen systemer fundet." msgstr "Ingen systemer fundet."
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications" msgid "Notifications"
msgstr "Notifikationer" msgstr "Notifikationer"
@@ -706,9 +672,9 @@ msgstr "OAuth 2 / OIDC understøttelse"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file." msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen." msgstr "Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen."
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu" msgid "Open menu"
msgstr "Åbn menu" msgstr "Åbn menu"
@@ -716,7 +682,7 @@ msgstr "Åbn menu"
msgid "Or continue with" msgid "Or continue with"
msgstr "Eller fortsæt med" msgstr "Eller fortsæt med"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts" msgid "Overwrite existing alerts"
msgstr "Overskriv eksisterende alarmer" msgstr "Overskriv eksisterende alarmer"
@@ -756,7 +722,6 @@ msgid "Pause"
msgstr "Pause" msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused" msgid "Paused"
msgstr "Sat på pause" msgstr "Sat på pause"
@@ -764,12 +729,12 @@ msgstr "Sat på pause"
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Konfigurer <0>en SMTP server</0> for at sikre at alarmer bliver leveret." msgstr "Konfigurer <0>en SMTP server</0> for at sikre at alarmer bliver leveret."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "Tjek logfiler for flere detaljer." msgstr "Tjek logfiler for flere detaljer."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "Tjek dine legitimationsoplysninger og prøv igen" msgstr "Tjek dine legitimationsoplysninger og prøv igen"
@@ -781,7 +746,7 @@ msgstr "Opret venligst en administratorkonto"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Aktiver pop-ups for dette websted" msgstr "Aktiver pop-ups for dette websted"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Log venligst ind igen" msgstr "Log venligst ind igen"
@@ -847,8 +812,8 @@ msgstr ""
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser." msgstr "Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser."
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "Gem indstillinger" msgstr "Gem indstillinger"
@@ -864,7 +829,7 @@ msgstr "Søg"
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "Søg efter systemer eller indstillinger..." msgstr "Søg efter systemer eller indstillinger..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtager alarmer." msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtager alarmer."
@@ -908,8 +873,7 @@ msgstr "Sorter efter"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx #: src/lib/utils.ts
#: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -921,10 +885,12 @@ msgstr "Swap plads brugt af systemet"
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap forbrug" msgstr "Swap forbrug"
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System" msgid "System"
msgstr "System" msgstr "System"
@@ -949,8 +915,8 @@ msgstr "Tabel"
msgid "Temp" msgid "Temp"
msgstr "Temperatur" msgstr "Temperatur"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatur" msgstr "Temperatur"
@@ -1009,8 +975,8 @@ msgid "Token"
msgstr "" msgstr ""
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "" msgstr ""
@@ -1022,39 +988,39 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "" msgstr ""
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
msgstr "Udløser når en sensor overstiger en tærskel" msgstr "Udløser når en sensor overstiger en tærskel"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Udløses når de kombinerede op/ned overstiger en tærskel" msgstr "Udløses når de kombinerede op/ned overstiger en tærskel"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Udløser når CPU-forbrug overstiger en tærskel" msgstr "Udløser når CPU-forbrug overstiger en tærskel"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold" msgid "Triggers when memory usage exceeds a threshold"
msgstr "Udløser når hukommelsesforbruget overstiger en tærskel" msgstr "Udløser når hukommelsesforbruget overstiger en tærskel"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "Udløser når status skifter mellem op og ned" msgstr "Udløser når status skifter mellem op og ned"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Udløser når brugen af en disk overstiger en tærskel" msgstr "Udløser når brugen af en disk overstiger en tærskel"
@@ -1067,15 +1033,9 @@ msgstr ""
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Unknown"
msgstr "Ukendt"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
msgid "Up" msgid "Up"
msgstr "Oppe" msgstr "Oppe"
@@ -1098,13 +1058,13 @@ msgstr "Forbrug"
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Brug af rodpartition" msgstr "Brug af rodpartition"
#: src/components/charts/mem-chart.tsx
#: src/components/charts/swap-chart.tsx #: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
msgid "Used" msgid "Used"
msgstr "Brugt" msgstr "Brugt"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users" msgid "Users"
msgstr "Brugere" msgstr "Brugere"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n" "Language: de\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-25 01:15\n" "PO-Revision-Date: 2025-07-25 22:44\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -33,10 +33,6 @@ msgstr "{0} von {1} Zeile(n) ausgewählt."
msgid "{hours, plural, one {# hour} other {# hours}}" msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Stunde} other {# Stunden}}" msgstr "{hours, plural, one {# Stunde} other {# Stunden}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 Stunde" msgstr "1 Stunde"
@@ -73,8 +69,8 @@ msgid "5 min"
msgstr "5 Min" msgstr "5 Min"
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Actions" msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
@@ -117,19 +113,18 @@ msgid "Agent"
msgstr "Agent" msgstr "Agent"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Alert History" msgid "Alert History"
msgstr "Alarm-Verlauf" msgstr "Alarm-Verlauf"
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Alerts" msgid "Alerts"
msgstr "Warnungen" msgstr "Warnungen"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/alerts/alert-button.tsx
msgid "All Systems" msgid "All Systems"
msgstr "Alle Systeme" msgstr "Alle Systeme"
@@ -153,8 +148,8 @@ msgstr "Durchschnitt"
msgid "Average CPU utilization of containers" msgid "Average CPU utilization of containers"
msgstr "Durchschnittliche CPU-Auslastung der Container" msgstr "Durchschnittliche CPU-Auslastung der Container"
#. placeholder {0}: alertData.unit #. placeholder {0}: data.alert.unit
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Average exceeds <0>{value}{0}</0>" msgid "Average exceeds <0>{value}{0}</0>"
msgstr "Durchschnitt überschreitet <0>{value}{0}</0>" msgstr "Durchschnitt überschreitet <0>{value}{0}</0>"
@@ -171,20 +166,16 @@ msgstr "Durchschnittliche systemweite CPU-Auslastung"
msgid "Average utilization of {0}" msgid "Average utilization of {0}"
msgstr "Durchschnittliche Auslastung von {0}" msgstr "Durchschnittliche Auslastung von {0}"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Backups" msgid "Backups"
msgstr "Backups" msgstr "Backups"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Bandwidth" msgid "Bandwidth"
msgstr "Bandbreite" msgstr "Bandbreite"
#: src/components/routes/system.tsx
msgid "Battery"
msgstr "Batterie"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers." msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
msgstr "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter." msgstr "Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter."
@@ -211,8 +202,8 @@ msgstr "Bytes (KB/s, MB/s, GB/s)"
msgid "Cache / Buffers" msgid "Cache / Buffers"
msgstr "Cache / Puffer" msgstr "Cache / Puffer"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
@@ -232,15 +223,6 @@ msgstr "Anzeigeeinheiten der Werte ändern."
msgid "Change general application options." msgid "Change general application options."
msgstr "Allgemeine Anwendungsoptionen ändern." msgstr "Allgemeine Anwendungsoptionen ändern."
#: src/components/routes/system.tsx
msgid "Charge"
msgstr "Ladung"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Charging"
msgstr "Wird geladen"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
msgstr "Diagrammoptionen" msgstr "Diagrammoptionen"
@@ -279,8 +261,8 @@ msgstr "Passwort bestätigen"
msgid "Connection is down" msgid "Connection is down"
msgstr "Verbindung unterbrochen" msgstr "Verbindung unterbrochen"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Continue" msgid "Continue"
msgstr "Fortfahren" msgstr "Fortfahren"
@@ -338,9 +320,9 @@ msgstr "YAML kopieren"
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr "CPU"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "CPU Usage" msgid "CPU Usage"
msgstr "CPU-Auslastung" msgstr "CPU-Auslastung"
@@ -357,10 +339,10 @@ msgstr "Erstellt"
msgid "Critical (%)" msgid "Critical (%)"
msgstr "Kritisch (%)" msgstr "Kritisch (%)"
#. Context: Battery state #. Dark theme
#: src/components/routes/system.tsx #: src/components/mode-toggle.tsx
msgid "Current state" msgid "Dark"
msgstr "Aktueller Zustand" msgstr "Dunkel"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -371,8 +353,8 @@ msgstr "Dashboard"
msgid "Default time period" msgid "Default time period"
msgstr "Standardzeitraum" msgstr "Standardzeitraum"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@@ -380,11 +362,6 @@ msgstr "Löschen"
msgid "Delete fingerprint" msgid "Delete fingerprint"
msgstr "Fingerabdruck löschen" msgstr "Fingerabdruck löschen"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Discharging"
msgstr "Wird entladen"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
msgstr "Festplatte" msgstr "Festplatte"
@@ -397,9 +374,9 @@ msgstr "Festplatten-I/O"
msgid "Disk unit" msgid "Disk unit"
msgstr "Festplatteneinheit" msgstr "Festplatteneinheit"
#: src/components/charts/disk-chart.tsx #: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/components/charts/disk-chart.tsx
msgid "Disk Usage" msgid "Disk Usage"
msgstr "Festplattennutzung" msgstr "Festplattennutzung"
@@ -424,11 +401,10 @@ msgid "Documentation"
msgstr "Dokumentation" msgstr "Dokumentation"
#. Context: System is down #. Context: System is down
#: src/lib/utils.ts
#: src/components/alerts-history-columns.tsx #: 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-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Down" msgid "Down"
msgstr "Offline" msgstr "Offline"
@@ -441,8 +417,8 @@ msgstr "Dauer"
msgid "Edit" msgid "Edit"
msgstr "Bearbeiten" msgstr "Bearbeiten"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Email" msgid "Email"
msgstr "E-Mail" msgstr "E-Mail"
@@ -450,11 +426,6 @@ msgstr "E-Mail"
msgid "Email notifications" msgid "Email notifications"
msgstr "E-Mail-Benachrichtigungen" msgstr "E-Mail-Benachrichtigungen"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Empty"
msgstr "Leer"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen" msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
@@ -463,11 +434,11 @@ msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
msgid "Enter email address..." msgid "Enter email address..."
msgstr "E-Mail-Adresse eingeben..." msgstr "E-Mail-Adresse eingeben..."
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/login/auth-form.tsx
msgid "Error" msgid "Error"
msgstr "Fehler" msgstr "Fehler"
@@ -498,12 +469,12 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr "Fahrenheit (°F)"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Authentifizierung fehlgeschlagen" msgstr "Authentifizierung fehlgeschlagen"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Failed to save settings" msgid "Failed to save settings"
msgstr "Einstellungen konnten nicht gespeichert werden" msgstr "Einstellungen konnten nicht gespeichert werden"
@@ -511,13 +482,13 @@ msgstr "Einstellungen konnten nicht gespeichert werden"
msgid "Failed to send test notification" msgid "Failed to send test notification"
msgstr "Testbenachrichtigung konnte nicht gesendet werden" msgstr "Testbenachrichtigung konnte nicht gesendet werden"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Failed to update alert" msgid "Failed to update alert"
msgstr "Warnung konnte nicht aktualisiert werden" msgstr "Warnung konnte nicht aktualisiert werden"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/routes/system.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "Filter..." msgid "Filter..."
msgstr "Filter..." msgstr "Filter..."
@@ -525,7 +496,7 @@ msgstr "Filter..."
msgid "Fingerprint" msgid "Fingerprint"
msgstr "Fingerabdruck" msgstr "Fingerabdruck"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}" msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
@@ -533,14 +504,9 @@ msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
msgid "Forgot password?" msgid "Forgot password?"
msgstr "Passwort vergessen?" msgstr "Passwort vergessen?"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr "Voll"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/general.tsx
msgid "General" msgid "General"
msgstr "Allgemein" msgstr "Allgemein"
@@ -562,11 +528,6 @@ msgstr "Homebrew-Befehl"
msgid "Host / IP" msgid "Host / IP"
msgstr "Host / IP" msgstr "Host / IP"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Idle"
msgstr "Untätig"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "If you've lost the password to your admin account, you may reset it using the following command." msgid "If you've lost the password to your admin account, you may reset it using the following command."
msgstr "Wenn du das Passwort für dein Administratorkonto verloren hast, kannst du es mit dem folgenden Befehl zurücksetzen." msgstr "Wenn du das Passwort für dein Administratorkonto verloren hast, kannst du es mit dem folgenden Befehl zurücksetzen."
@@ -588,26 +549,31 @@ msgstr "Sprache"
msgid "Layout" msgid "Layout"
msgstr "Anordnung" msgstr "Anordnung"
#. Light theme
#: src/components/mode-toggle.tsx
msgid "Light"
msgstr "Hell"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "Durchschnittliche Systemlast" msgstr "Durchschnittliche Systemlast"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "Durchschnittliche Systemlast 15 Min" msgstr "Durchschnittliche Systemlast 15 Min"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "Durchschnittliche Systemlast 1 Min" msgstr "Durchschnittliche Systemlast 1 Min"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "Durchschnittliche Systemlast 5 Min" msgstr "Durchschnittliche Systemlast 5 Min"
#. Short label for load average #. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg" msgid "Load Avg"
msgstr "Systemlast" msgstr "Durchschnittliche Last"
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
@@ -617,13 +583,13 @@ msgstr "Abmelden"
msgid "Login" msgid "Login"
msgstr "Anmelden" msgstr "Anmelden"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Login attempt failed" msgid "Login attempt failed"
msgstr "Anmeldeversuch fehlgeschlagen" msgstr "Anmeldeversuch fehlgeschlagen"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Logs" msgid "Logs"
msgstr "Protokolle" msgstr "Protokolle"
@@ -648,8 +614,8 @@ msgstr "Max 1 Min"
msgid "Memory" msgid "Memory"
msgstr "Arbeitsspeicher" msgstr "Arbeitsspeicher"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Arbeitsspeichernutzung" msgstr "Arbeitsspeichernutzung"
@@ -657,8 +623,8 @@ msgstr "Arbeitsspeichernutzung"
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Arbeitsspeichernutzung der Docker-Container" msgstr "Arbeitsspeichernutzung der Docker-Container"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/add-system.tsx
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
@@ -693,8 +659,8 @@ msgid "No systems found."
msgstr "Keine Systeme gefunden." msgstr "Keine Systeme gefunden."
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/layout.tsx
msgid "Notifications" msgid "Notifications"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
@@ -706,9 +672,9 @@ msgstr "OAuth 2 / OIDC-Unterstützung"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file." msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen." msgstr "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen."
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Open menu" msgid "Open menu"
msgstr "Menü öffnen" msgstr "Menü öffnen"
@@ -716,7 +682,7 @@ msgstr "Menü öffnen"
msgid "Or continue with" msgid "Or continue with"
msgstr "Oder fortfahren mit" msgstr "Oder fortfahren mit"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "Overwrite existing alerts" msgid "Overwrite existing alerts"
msgstr "Bestehende Warnungen überschreiben" msgstr "Bestehende Warnungen überschreiben"
@@ -756,7 +722,6 @@ msgid "Pause"
msgstr "Pause" msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused" msgid "Paused"
msgstr "Pausiert" msgstr "Pausiert"
@@ -764,12 +729,12 @@ msgstr "Pausiert"
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Bitte <0>konfiguriere einen SMTP-Server</0>, um sicherzustellen, dass Warnungen zugestellt werden." msgstr "Bitte <0>konfiguriere einen SMTP-Server</0>, um sicherzustellen, dass Warnungen zugestellt werden."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-system.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "Bitte überprüfe die Protokolle für weitere Details." msgstr "Bitte überprüfe die Protokolle für weitere Details."
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
#: src/components/login/auth-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "Bitte überprüfe deine Anmeldedaten und versuche es erneut" msgstr "Bitte überprüfe deine Anmeldedaten und versuche es erneut"
@@ -781,7 +746,7 @@ msgstr "Bitte erstelle ein Administratorkonto"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Bitte aktiviere Pop-ups für diese Seite" msgstr "Bitte aktiviere Pop-ups für diese Seite"
#: src/lib/api.ts #: src/lib/utils.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Bitte melde dich erneut an" msgstr "Bitte melde dich erneut an"
@@ -791,7 +756,7 @@ msgstr "In der <0>Dokumentation</0> findest du weitere Anweisungen."
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Please sign in to your account" msgid "Please sign in to your account"
msgstr "Bitte melde dich bei deinem Konto an" msgstr "Bitte melde dich bei beinem Konto an"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
@@ -847,8 +812,8 @@ msgstr "Zeilen pro Seite"
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren." msgstr "Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren."
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/general.tsx
msgid "Save Settings" msgid "Save Settings"
msgstr "Einstellungen speichern" msgstr "Einstellungen speichern"
@@ -864,7 +829,7 @@ msgstr "Suche"
msgid "Search for systems or settings..." msgid "Search for systems or settings..."
msgstr "Nach Systemen oder Einstellungen suchen..." msgstr "Nach Systemen oder Einstellungen suchen..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alert-button.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du Warnungen erhältst." msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du Warnungen erhältst."
@@ -908,8 +873,7 @@ msgstr "Sortieren nach"
msgid "State" msgid "State"
msgstr "Status" msgstr "Status"
#: src/components/systems-table/systems-table.tsx #: src/lib/utils.ts
#: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -921,10 +885,12 @@ msgstr "Vom System genutzter Swap-Speicher"
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Swap-Nutzung" msgstr "Swap-Nutzung"
#. System theme
#: src/lib/utils.ts
#: src/components/mode-toggle.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "System" msgid "System"
msgstr "System" msgstr "System"
@@ -949,8 +915,8 @@ msgstr "Tabelle"
msgid "Temp" msgid "Temp"
msgstr "Temperatur" msgstr "Temperatur"
#: src/lib/utils.ts
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts
msgid "Temperature" msgid "Temperature"
msgstr "Temperatur" msgstr "Temperatur"
@@ -1009,8 +975,8 @@ msgid "Token"
msgstr "Token" msgstr "Token"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/layout.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "Tokens & Fingerabdrücke" msgstr "Tokens & Fingerabdrücke"
@@ -1022,39 +988,39 @@ msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fi
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren." msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten Minute einen Schwellenwert überschreitet" msgstr "Löst aus, wenn der Lastdurchschnitt der letzten Minute einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwellenwert überschreitet" msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet" msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet" msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when combined up/down exceeds a threshold" msgid "Triggers when combined up/down exceeds a threshold"
msgstr "Löst aus, wenn die kombinierte Auf-/Abwärtsbewegung einen Schwellenwert überschreitet" msgstr "Löst aus, wenn die kombinierte Auf-/Abwärtsbewegung einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when CPU usage exceeds a threshold" msgid "Triggers when CPU usage exceeds a threshold"
msgstr "Löst aus, wenn die CPU-Auslastung einen Schwellenwert überschreitet" msgstr "Löst aus, wenn die CPU-Auslastung einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when memory usage exceeds a threshold" msgid "Triggers when memory usage exceeds a threshold"
msgstr "Löst aus, wenn die Arbeitsspeichernutzung einen Schwellenwert überschreitet" msgstr "Löst aus, wenn die Arbeitsspeichernutzung einen Schwellenwert überschreitet"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when status switches between up and down" msgid "Triggers when status switches between up and down"
msgstr "Löst aus, wenn der Status zwischen online und offline wechselt" msgstr "Löst aus, wenn der Status zwischen online und offline wechselt"
#: src/lib/alerts.ts #: src/lib/utils.ts
msgid "Triggers when usage of any disk exceeds a threshold" msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet" msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet"
@@ -1067,15 +1033,9 @@ msgstr "Einheiten"
msgid "Universal token" msgid "Universal token"
msgstr "Universeller Token" msgstr "Universeller Token"
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Unknown"
msgstr "Unbekannt"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/routes/system.tsx
msgid "Up" msgid "Up"
msgstr "aktiv" msgstr "aktiv"
@@ -1098,13 +1058,13 @@ msgstr "Nutzung"
msgid "Usage of root partition" msgid "Usage of root partition"
msgstr "Nutzung der Root-Partition" msgstr "Nutzung der Root-Partition"
#: src/components/charts/mem-chart.tsx
#: src/components/charts/swap-chart.tsx #: src/components/charts/swap-chart.tsx
#: src/components/charts/mem-chart.tsx
msgid "Used" msgid "Used"
msgstr "Verwendet" msgstr "Verwendet"
#: src/components/command-palette.tsx
#: src/components/navbar.tsx #: src/components/navbar.tsx
#: src/components/command-palette.tsx
msgid "Users" msgid "Users"
msgstr "Benutzer" msgstr "Benutzer"

Some files were not shown because too many files have changed in this diff Show More