Compare commits

..

48 Commits

Author SHA1 Message Date
henrygd
5e37469ea9 0.12.7 release :) 2025-09-05 14:00:24 -04:00
henrygd
e027479bb1 make sure initial user is verfied when supplying user/pass 2025-09-05 14:00:21 -04:00
henrygd
1597e869c1 update translations 2025-09-05 13:53:17 -04:00
henrygd
862399d8ec update language files 2025-09-05 12:36:45 -04:00
henrygd
f6f85f8f9d Add USER_EMAIL and USER_PASSWORD env vars to set the email / pass of initial user (#1137) 2025-09-05 11:42:43 -04:00
Riedel, Max
e22d7ca801 fix: add nextSystemToken to deps of useEffect to generate a new token after system creation (#1142) 2025-09-05 11:00:32 -04:00
henrygd
c382c1d5f6 windows: make LHM opt-in with LHM=true (#1130) 2025-09-05 10:39:18 -04:00
henrygd
f7618ed6b0 update go version for vulcheck action 2025-09-04 19:17:44 -04:00
henrygd
d1295b7c50 alerts tests and small refactoring 2025-09-04 19:13:10 -04:00
henrygd
a162a54a58 bump go version and add keyword 2025-09-04 19:13:10 -04:00
henrygd
794db0ac6a make sure old names are removed in systemsbyname store 2025-09-04 19:13:10 -04:00
henrygd
e9fb9b856f install script: remove newlines from KEY (#1139) 2025-09-04 11:26:53 -04:00
Sven van Ginkel
66bca11d36 [Bug] Update install script to use crontab on Alpine (#1136)
* add cron

* update the install script
2025-09-03 23:10:38 -04:00
henrygd
86e87f0d47 refactor hub dev server
- moved html replacement functionality from vite to go
2025-09-01 22:16:57 -04:00
henrygd
fadfc5d81d refactor(hub): separate development and production server logic 2025-09-01 19:27:11 -04:00
henrygd
fc39ff1e4d add pflag to go deps 2025-09-01 19:24:26 -04:00
henrygd
82ccfc66e0 refactor: shared container charts config hook 2025-09-01 18:41:30 -04:00
henrygd
890bad1c39 refactor: improve runOnce with weakmap cache 2025-09-01 18:34:29 -04:00
henrygd
9c458885f1 refactor (hub): add systemsManager module
- Removed the `updateSystemList` function and replaced it with a more efficient system management approach using `systemsManager`.
- Updated the `App` component to initialize and subscribe to system updates through the new `systemsManager`.
- Refactored the `SystemsTable` and `SystemDetail` components to utilize the new state management for systems, improving performance and maintainability.
- Enhanced the `ActiveAlerts` component to fetch system names directly from the new state structure.
2025-09-01 17:29:33 -04:00
henrygd
d2aed0dc72 refactor: replace useLocalStorage with useBrowserStorage 2025-09-01 17:28:13 -04:00
Augustin ROLET
3dbcb5d7da Minor UI changes (#1110)
* ui: add devices count from #1078

* ux: save sortMode in localStorage from #1024

* fix: reload component when system switch to "up"

* ux: move running systems to desc field
2025-08-31 18:16:25 -04:00
Alexander Mnich
57a1a8b39e [Feature] improved support for mips and mipsle architectures (#1112)
* switch mipsle to softfloat

* feat: add support for mips
2025-08-30 15:50:15 -04:00
Alexander Mnich
ab81c04569 [Fix] fix GitHub workflow errors in forks (#1113)
* feat: do not run winget/homebrew/scoop release in fork

* fix: replaced deprecated goreleaser fields

https://goreleaser.com/deprecations/#archivesbuilds

* fix: push docker images only with access to the registry
2025-08-30 15:49:49 -04:00
henrygd
0c32be3bea 0.12.6 release :) 2025-08-29 17:24:45 -04:00
henrygd
81d43fbf6e refactor: small style improvements 2025-08-29 17:23:47 -04:00
henrygd
96f441de40 Virtualize All Systems table to improve performance with hundreds of systems (#1100)
- Also truncate long system names in tables and alerts sheet. (#1104)
2025-08-29 16:16:45 -04:00
henrygd
0e95caaee9 update command ui component 2025-08-29 15:04:26 -04:00
Sven van Ginkel
7697a12b42 fix alignment for metrics (#1109) 2025-08-29 14:00:17 -04:00
henrygd
94245a9ba4 fix update mirror and make opt-in with --china-mirrors (#1035) 2025-08-29 13:46:24 -04:00
henrygd
b084814aea auth form: fix border style and add theme toggle 2025-08-28 21:17:44 -04:00
Impact
cce74246ee Use older cuda image for increased compatibility (#1103) 2025-08-28 20:49:52 -04:00
henrygd
a3420b8c67 add max 1 min memory 2025-08-28 20:07:22 -04:00
henrygd
e1bb17ee9e update locale files 2025-08-28 18:23:40 -04:00
henrygd
52983f60b7 refactor: add api module and page preloading 2025-08-28 18:23:24 -04:00
henrygd
1f053fd85d update 2025-08-28 17:31:18 -04:00
Sven van Ginkel
a989d121d3 [Feature] Add Status Filtering to Systems Table (#927) 2025-08-28 17:30:44 -04:00
Sven van Ginkel
50d2406423 [Bug] Fix system table in Safari (#1092)
Co-authored-by: henrygd <hank@henrygd.me>
2025-08-28 12:07:27 -04:00
Sven van Ginkel
059d2d0a5b Add missing os.Chmod step to hub update command (#1093) 2025-08-27 13:00:03 -04:00
henrygd
621bef30b5 update changelog 2025-08-26 21:26:18 -04:00
henrygd
5f4d3dc730 0.12.5 release :) 2025-08-26 21:04:46 -04:00
henrygd
8fa9aece63 change long german translation 2025-08-26 20:50:35 -04:00
henrygd
2f1a022e2a refactor: use width for meters instead of scale 2025-08-26 20:49:31 -04:00
henrygd
4815cd29bc ghupdate: rename plugin struct 2025-08-26 18:41:42 -04:00
henrygd
e49bfaf5d7 downgrade gopsutil to fix freebsd bug (#1083) 2025-08-26 18:40:32 -04:00
henrygd
b13915b76f freebsd: fix battery-related bug (#1081) 2025-08-26 18:39:42 -04:00
henrygd
e2a57dc43b update tooltip component for tailwind 4 2025-08-26 16:16:38 -04:00
henrygd
7222224b40 add battery to supported metrics 2025-08-25 23:15:19 -04:00
henrygd
02ff475b84 improve language toggle selected style 2025-08-25 22:14:48 -04:00
111 changed files with 2938 additions and 1230 deletions

View File

@@ -93,7 +93,9 @@ jobs:
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' env:
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] }}
@@ -108,6 +110,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' }} push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}

View File

@@ -51,3 +51,4 @@ 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

@@ -15,7 +15,7 @@ permissions:
jobs: jobs:
vulncheck: vulncheck:
name: Analysis name: VulnCheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
@@ -23,8 +23,8 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.24.x go-version: 1.25.x
cached: false # cached: false
- name: Get official govulncheck - name: Get official govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest run: go install golang.org/x/vuln/cmd/govulncheck@latest
shell: bash shell: bash

View File

@@ -38,12 +38,25 @@ 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
@@ -54,7 +67,7 @@ builds:
archives: archives:
- id: beszel-agent - id: beszel-agent
formats: [tar.gz] formats: [tar.gz]
builds: ids:
- beszel-agent - beszel-agent
name_template: >- name_template: >-
{{ .Binary }}_ {{ .Binary }}_
@@ -66,7 +79,7 @@ archives:
- id: beszel - id: beszel
formats: [tar.gz] formats: [tar.gz]
builds: ids:
- beszel - beszel
name_template: >- name_template: >-
{{ .Binary }}_ {{ .Binary }}_
@@ -85,7 +98,7 @@ nfpms:
API access. API access.
maintainer: henrygd <hank@henrygd.me> maintainer: henrygd <hank@henrygd.me>
section: net section: net
builds: ids:
- beszel-agent - beszel-agent
formats: formats:
- deb - deb
@@ -122,6 +135,7 @@ 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:
@@ -155,7 +169,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: auto skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
extra_install: | extra_install: |
(bin/"beszel-agent-launcher").write <<~EOS (bin/"beszel-agent-launcher").write <<~EOS
#!/bin/bash #!/bin/bash
@@ -187,7 +201,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: auto skip_upload: "{{ if .Env.IS_FORK }}true{{ else }}auto{{ end }}"
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

@@ -7,7 +7,7 @@ SKIP_WEB ?= false
# Set executable extension based on target OS # Set executable extension based on target OS
EXE_EXT := $(if $(filter windows,$(OS)),.exe,) EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales .PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
clean: clean:
@@ -53,6 +53,10 @@ build-agent: tidy build-dotnet-conditional
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui) build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
build-hub-dev: tidy
mkdir -p ./site/dist && touch ./site/dist/index.html
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
build: build-agent build-hub build: build-agent build-hub
generate-locales: generate-locales:
@@ -73,9 +77,9 @@ dev-hub: export ENV=dev
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 \
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \ find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
else \ else \
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \ cd ./cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
fi fi
dev-agent: dev-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,43 +17,24 @@ 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 {
@@ -63,7 +44,57 @@ func (opts *cmdOptions) parse() bool {
return true return true
} }
flag.Parse() // pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use")
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility
flagsToConvert := []string{"key", "listen"}
for i, arg := range os.Args {
for _, flag := range flagsToConvert {
singleDash := "-" + flag
doubleDash := "--" + flag
if arg == singleDash {
os.Args[i] = doubleDash
break
} else if strings.HasPrefix(arg, singleDash+"=") {
os.Args[i] = doubleDash + arg[len(singleDash):]
break
}
}
}
pflag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
// builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
pflag.PrintDefaults()
}
// Parse all arguments with pflag
pflag.Parse()
// Must run after pflag.Parse()
switch {
case *help || subcommand == "help":
pflag.Usage()
return true
case subcommand == "update":
agent.Update(*chinaMirrors)
return true
}
return false 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
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
}() }()
tests := []struct { tests := []struct {
@@ -269,6 +269,22 @@ 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"},
@@ -277,6 +293,22 @@ 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"},
@@ -290,12 +322,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
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError) pflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)
os.Args = tt.args os.Args = tt.args
var opts cmdOptions var opts cmdOptions
opts.parse() opts.parse()
flag.Parse() pflag.Parse()
assert.Equal(t, tt.expected, opts) assert.Equal(t, tt.expected, opts)
}) })

View File

@@ -45,11 +45,13 @@ 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
baseApp.RootCmd.AddCommand(&cobra.Command{ updateCmd := &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

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

View File

@@ -1,6 +1,6 @@
module beszel module beszel
go 1.24.4 go 1.25.1
// lock shoutrrr to specific version to allow review before updating // lock shoutrrr to specific version to allow review before updating
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8 replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
@@ -15,9 +15,10 @@ require (
github.com/nicholas-fedor/shoutrrr v0.8.17 github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3 github.com/pocketbase/pocketbase v0.29.3
github.com/shirou/gopsutil/v4 v4.25.7 github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/cast v1.9.2 github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.0 github.com/stretchr/testify v1.11.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
@@ -49,7 +50,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
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/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/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,9 +46,10 @@ var lhmFs embed.FS
var ( var (
beszelLhm *lhmProcess beszelLhm *lhmProcess
beszelLhmOnce sync.Once beszelLhmOnce sync.Once
useLHM = os.Getenv("LHM") == "true"
) )
var errNoSensors = errors.New("no sensors found (try running as admin)") var errNoSensors = errors.New("no sensors found (try running as admin with LHM=true)")
// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it. // newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.
func newlhmProcess() (*lhmProcess, error) { func newlhmProcess() (*lhmProcess, error) {
@@ -139,7 +140,7 @@ func (lhm *lhmProcess) cleanupProcess() {
} }
func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) { func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
if lhm.stoppedNoSensors { if !useLHM || lhm.stoppedNoSensors {
// Fall back to gopsutil if we can't get sensors from LHM // Fall back to gopsutil if we can't get sensors from LHM
return sensors.TemperaturesWithContext(ctx) return sensors.TemperaturesWithContext(ctx)
} }
@@ -222,6 +223,10 @@ func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err e
} }
}() }()
if !useLHM {
return sensors.TemperaturesWithContext(ctx)
}
// Initialize process once // Initialize process once
beszelLhmOnce.Do(func() { beszelLhmOnce.Do(func() {
beszelLhm, err = newlhmProcess() beszelLhm, err = newlhmProcess()

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ var supportsTitle = map[string]struct{}{
func NewAlertManager(app hubLike) *AlertManager { func NewAlertManager(app hubLike) *AlertManager {
am := &AlertManager{ am := &AlertManager{
hub: app, hub: app,
alertQueue: make(chan alertTask), alertQueue: make(chan alertTask, 5),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
am.bindEvents() am.bindEvents()

View File

@@ -42,21 +42,10 @@ func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
// resolveAlertHistoryRecord sets the resolved field to the current time // resolveAlertHistoryRecord sets the resolved field to the current time
func resolveAlertHistoryRecord(app core.App, alertRecordID string) error { func resolveAlertHistoryRecord(app core.App, alertRecordID string) error {
alertHistoryRecords, err := app.FindRecordsByFilter( alertHistoryRecord, err := app.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id} && resolved=null", dbx.Params{"alert_id": alertRecordID})
"alerts_history", if err != nil || alertHistoryRecord == nil {
"alert_id={:alert_id} && resolved=null",
"-created",
1,
0,
dbx.Params{"alert_id": alertRecordID},
)
if err != nil {
return err return err
} }
if len(alertHistoryRecords) == 0 {
return nil
}
alertHistoryRecord := alertHistoryRecords[0] // there should be only one record
alertHistoryRecord.Set("resolved", time.Now().UTC()) alertHistoryRecord.Set("resolved", time.Now().UTC())
err = app.Save(alertHistoryRecord) err = app.Save(alertHistoryRecord)
if err != nil { if err != nil {

View File

@@ -10,6 +10,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"testing/synctest"
"time"
beszelTests "beszel/internal/tests" beszelTests "beszel/internal/tests"
@@ -63,14 +65,14 @@ func TestUserAlertsApi(t *testing.T) {
} }
scenarios := []beszelTests.ApiScenario{ scenarios := []beszelTests.ApiScenario{
{ // {
Name: "GET not implemented - returns index", // Name: "GET not implemented - returns index",
Method: http.MethodGet, // Method: http.MethodGet,
URL: "/api/beszel/user-alerts", // URL: "/api/beszel/user-alerts",
ExpectedStatus: 200, // ExpectedStatus: 200,
ExpectedContent: []string{"<html ", "globalThis.BESZEL"}, // ExpectedContent: []string{"<html ", "globalThis.BESZEL"},
TestAppFactory: testAppFactory, // TestAppFactory: testAppFactory,
}, // },
{ {
Name: "POST no auth", Name: "POST no auth",
Method: http.MethodPost, Method: http.MethodPost,
@@ -366,3 +368,237 @@ func TestUserAlertsApi(t *testing.T) {
scenario.Test(t) scenario.Test(t)
} }
} }
func getHubWithUser(t *testing.T) (*beszelTests.TestHub, *core.Record) {
hub, err := beszelTests.NewTestHub(t.TempDir())
assert.NoError(t, err)
hub.StartHub()
// Manually initialize the system manager to bind event hooks
err = hub.GetSystemManager().Initialize()
assert.NoError(t, err)
// Create a test user
user, err := beszelTests.CreateUser(hub, "test@example.com", "password")
assert.NoError(t, err)
// Create user settings for the test user (required for alert notifications)
userSettingsData := map[string]any{
"user": user.Id,
"settings": `{"emails":[test@example.com],"webhooks":[]}`,
}
_, err = beszelTests.CreateRecord(hub, "user_settings", userSettingsData)
assert.NoError(t, err)
return hub, user
}
func TestStatusAlerts(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
defer hub.Cleanup()
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
assert.NoError(t, err)
var alerts []*core.Record
for i, system := range systems {
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": i + 1,
})
assert.NoError(t, err)
alerts = append(alerts, alert)
}
time.Sleep(10 * time.Millisecond)
for _, alert := range alerts {
assert.False(t, alert.GetBool("triggered"), "Alert should not be triggered immediately")
}
if hub.TestMailer.TotalSend() != 0 {
assert.Zero(t, hub.TestMailer.TotalSend(), "Expected 0 messages, got %d", hub.TestMailer.TotalSend())
}
for _, system := range systems {
assert.EqualValues(t, "paused", system.GetString("status"), "System should be paused")
}
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
for _, system := range systems {
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
// after 30 seconds, should have 4 alerts in the pendingAlerts map, no triggered alerts
time.Sleep(time.Second * 30)
assert.EqualValues(t, 4, hub.GetPendingAlertsCount(), "should have 4 alerts in the pendingAlerts map")
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 0, triggeredCount, "should have 0 alert triggered")
assert.EqualValues(t, 0, hub.TestMailer.TotalSend(), "should have 0 messages sent")
// after 1:30 seconds, should have 1 triggered alert and 3 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 3, hub.GetPendingAlertsCount(), "should have 3 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "should have 1 alert triggered")
assert.EqualValues(t, 1, hub.TestMailer.TotalSend(), "should have 1 messages sent")
// after 2:30 seconds, should have 2 triggered alerts and 2 pending alerts
time.Sleep(time.Second * 60)
assert.EqualValues(t, 2, hub.GetPendingAlertsCount(), "should have 2 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.EqualValues(t, 2, triggeredCount, "should have 2 alert triggered")
assert.EqualValues(t, 2, hub.TestMailer.TotalSend(), "should have 2 messages sent")
// now we will bring the remaning systems back up
for _, system := range systems {
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
}
time.Sleep(time.Second)
// should have 0 alerts in the pendingAlerts map and 0 alerts triggered
assert.EqualValues(t, 0, hub.GetPendingAlertsCount(), "should have 0 alerts in the pendingAlerts map")
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "should have 0 alert triggered")
// 4 messages sent, 2 down alerts and 2 up alerts for first 2 systems
assert.EqualValues(t, 4, hub.TestMailer.TotalSend(), "should have 4 messages sent")
})
}
func TestAlertsHistory(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := getHubWithUser(t)
defer hub.Cleanup()
// Create systems and alerts
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Initially, no alert history records should exist
initialHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.Zero(t, initialHistoryCount, "Should have 0 alert history records initially")
// Set system to up initially
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
// Set system to down to trigger alert
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
// Wait for alert to trigger (after the downtime delay)
// With 1 minute delay, we need to wait at least 1 minute + some buffer
time.Sleep(time.Second * 75)
// Check that alert is triggered
triggeredCount, err := hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Alert should be triggered")
// Check that alert history record was created
historyCount, err := hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for triggered alert")
// Get the alert history record and verify it's not resolved immediately
historyRecord, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should exist")
assert.Equal(t, alert.Id, historyRecord.GetString("alert_id"), "Alert history should reference correct alert")
assert.Equal(t, system.Id, historyRecord.GetString("system"), "Alert history should reference correct system")
assert.Equal(t, "Status", historyRecord.GetString("name"), "Alert history should have correct name")
// The alert history might be resolved immediately in some cases, so let's check the alert's triggered status
alertRecord, err := hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": alert.Id})
assert.NoError(t, err)
assert.True(t, alertRecord.GetBool("triggered"), "Alert should still be triggered when checking history")
// Now resolve the alert by setting system back to up
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(200 * time.Millisecond)
// Check that alert is no longer triggered
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert.Id})
assert.NoError(t, err)
assert.Zero(t, triggeredCount, "Alert should not be triggered after system is back up")
// Check that alert history record is now resolved
historyRecord, err = hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord, "Alert history record should still exist")
assert.NotNil(t, historyRecord.Get("resolved"), "Alert history should be resolved")
// Test deleting a triggered alert resolves its history
// Create another system and alert
systems2, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system2 := systems2[0]
system2.Set("name", "test-system-2") // Rename for clarity
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
alert2, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system2.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Set system2 to down to trigger alert
system2.Set("status", "down")
err = hub.SaveNoValidate(system2)
assert.NoError(t, err)
// Wait for alert to trigger
time.Sleep(time.Second * 75)
// Verify alert is triggered and history record exists
triggeredCount, err = hub.CountRecords("alerts", dbx.HashExp{"triggered": true, "id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, triggeredCount, "Second alert should be triggered")
historyCount, err = hub.CountRecords("alerts_history", dbx.HashExp{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.EqualValues(t, 1, historyCount, "Should have 1 alert history record for second alert")
// Delete the triggered alert
err = hub.Delete(alert2)
assert.NoError(t, err)
// Check that alert history record is resolved after deletion
historyRecord2, err := hub.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id}", dbx.Params{"alert_id": alert2.Id})
assert.NoError(t, err)
assert.NotNil(t, historyRecord2, "Alert history record should still exist after alert deletion")
assert.NotNil(t, historyRecord2.Get("resolved"), "Alert history should be resolved after alert deletion")
// Verify total history count is correct (2 records total)
totalHistoryCount, err := hub.CountRecords("alerts_history", nil)
assert.NoError(t, err)
assert.EqualValues(t, 2, totalHistoryCount, "Should have 2 total alert history records")
})
}

View File

@@ -0,0 +1,55 @@
package alerts
import (
"sync"
"time"
"github.com/pocketbase/pocketbase/core"
)
func (am *AlertManager) GetAlertManager() *AlertManager {
return am
}
func (am *AlertManager) GetPendingAlerts() *sync.Map {
return &am.pendingAlerts
}
func (am *AlertManager) GetPendingAlertsCount() int {
count := 0
am.pendingAlerts.Range(func(key, value any) bool {
count++
return true
})
return count
}
// ProcessPendingAlerts manually processes all expired alerts (for testing)
func (am *AlertManager) ProcessPendingAlerts() ([]*core.Record, error) {
now := time.Now()
var lastErr error
var processedAlerts []*core.Record
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
if now.After(info.expireTime) {
// Downtime delay has passed, process alert
if err := am.sendStatusAlert("down", info.systemName, info.alertRecord); err != nil {
lastErr = err
}
processedAlerts = append(processedAlerts, info.alertRecord)
am.pendingAlerts.Delete(key)
}
return true
})
return processedAlerts, lastErr
}
// ForceExpirePendingAlerts sets all pending alerts to expire immediately (for testing)
func (am *AlertManager) ForceExpirePendingAlerts() {
now := time.Now()
am.pendingAlerts.Range(func(key, value any) bool {
info := value.(*alertInfo)
info.expireTime = now.Add(-time.Second) // Set to 1 second ago
return true
})
}

View File

@@ -39,6 +39,7 @@ type Stats struct {
// 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"` LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current] 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 {

View File

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

View File

@@ -8,13 +8,10 @@ import (
"beszel/internal/hub/systems" "beszel/internal/hub/systems"
"beszel/internal/records" "beszel/internal/records"
"beszel/internal/users" "beszel/internal/users"
"beszel/site"
"crypto/ed25519" "crypto/ed25519"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"net/http/httputil"
"net/url" "net/url"
"os" "os"
"path" "path"
@@ -115,6 +112,8 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
// set URL if BASE_URL env is set // set URL if BASE_URL env is set
if h.appURL != "" { if h.appURL != "" {
settings.Meta.AppURL = h.appURL settings.Meta.AppURL = h.appURL
} else {
h.appURL = settings.Meta.AppURL
} }
if err := e.App.Save(settings); err != nil { if err := e.App.Save(settings); err != nil {
return err return err
@@ -164,55 +163,6 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
return nil return nil
} }
// startServer sets up the server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// TODO: exclude dev server from production binary
switch h.IsDev() {
case true:
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
default:
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1)
indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, indexContent)
})
}
return nil
}
// registerCronJobs sets up scheduled tasks // registerCronJobs sets up scheduled tasks
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error { func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
// delete old system_stats and alerts_history records once every hour // delete old system_stats and alerts_history records once every hour

View File

@@ -5,6 +5,7 @@ package hub_test
import ( import (
beszelTests "beszel/internal/tests" beszelTests "beszel/internal/tests"
"beszel/migrations"
"testing" "testing"
"bytes" "bytes"
@@ -534,6 +535,115 @@ func TestApiRoutesAuthentication(t *testing.T) {
} }
} }
func TestFirstUserCreation(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenarios := []beszelTests.ApiScenario{
{
Name: "POST /create-user - should be available when no users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 200,
ExpectedContent: []string{"User created"},
TestAppFactory: testAppFactoryExisting,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.Zero(t, userCount, "Should start with no users")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser")
require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
userCount, err := hub.CountRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, userCount, "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should have created one superuser")
require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
},
{
Name: "POST /create-user - should not be available when users exist",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
Body: jsonReader(map[string]any{
"email": "firstuser@example.com",
"password": "password123",
}),
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactoryExisting,
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
})
t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) {
os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com")
os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123")
defer os.Unsetenv("BESZEL_HUB_USER_EMAIL")
defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
testAppFactory := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenario := beszelTests.ApiScenario{
Name: "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set",
Method: http.MethodPost,
URL: "/api/beszel/create-user",
ExpectedStatus: 404,
ExpectedContent: []string{"wasn't found"},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should start with one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should start with one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
users, err := hub.FindAllRecords("users")
require.NoError(t, err)
require.EqualValues(t, 1, len(users), "Should still have one user")
require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
require.NoError(t, err)
require.EqualValues(t, 1, len(superusers), "Should still have one superuser")
require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
},
}
scenario.Test(t)
})
}
func TestCreateUserEndpointAvailability(t *testing.T) { func TestCreateUserEndpointAvailability(t *testing.T) {
t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) { t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir()) hub, _ := beszelTests.NewTestHub(t.TempDir())

View File

@@ -0,0 +1,79 @@
//go:build development
package hub
import (
"beszel"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/pocketbase/pocketbase/core"
)
// Wraps http.RoundTripper to modify dev proxy HTML responses
type responseModifier struct {
transport http.RoundTripper
hub *Hub
}
func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := rm.transport.RoundTrip(req)
if err != nil {
return resp, err
}
// Only modify HTML responses
contentType := resp.Header.Get("Content-Type")
if !strings.Contains(contentType, "text/html") {
return resp, nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp, err
}
resp.Body.Close()
// Create a new response with the modified body
modifiedBody := rm.modifyHTML(string(body))
resp.Body = io.NopCloser(strings.NewReader(modifiedBody))
resp.ContentLength = int64(len(modifiedBody))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody)))
return resp, nil
}
func (rm *responseModifier) modifyHTML(html string) string {
parsedURL, err := url.Parse(rm.hub.appURL)
if err != nil {
return html
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
html = strings.ReplaceAll(html, "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1)
return html
}
// startServer sets up the development server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
slog.Info("starting server", "appURL", h.appURL)
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
proxy.Transport = &responseModifier{
transport: http.DefaultTransport,
hub: h,
}
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
proxy.ServeHTTP(e.Response, e.Request)
return nil
})
return nil
}

View File

@@ -0,0 +1,51 @@
//go:build !development
package hub
import (
"beszel"
"beszel/site"
"io/fs"
"net/http"
"net/url"
"strings"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)
// startServer sets up the production server for Beszel
func (h *Hub) startServer(se *core.ServeEvent) error {
// parse app url
parsedURL, err := url.Parse(h.appURL)
if err != nil {
return err
}
// fix base paths in html if using subpath
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
html := strings.ReplaceAll(string(indexFile), "./", basePath)
html = strings.Replace(html, "{{V}}", beszel.Version, 1)
html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1)
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
// get CSP configuration
csp, cspExists := GetEnv("CSP")
// add route
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
// serve static assets if path is in staticPaths
for i := range staticPaths {
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
return serveStatic(e)
}
}
if cspExists {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, html)
})
return nil
}

View File

@@ -100,3 +100,10 @@ func (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) boo
return true return true
} }
// TESTING ONLY: RemoveAllSystems removes all systems from the store
func (sm *SystemManager) RemoveAllSystems() {
for _, system := range sm.systems.GetAll() {
sm.RemoveSystem(system.Id)
}
}

View File

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

View File

@@ -214,6 +214,7 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Battery[1] = stats.Battery[1] 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)

View File

@@ -96,3 +96,31 @@ func ClearCollection(t testing.TB, app core.App, collectionName string) error {
assert.EqualValues(t, recordCount, 0, "should have 0 records after clearing") assert.EqualValues(t, recordCount, 0, "should have 0 records after clearing")
return err return err
} }
func (h *TestHub) Cleanup() {
h.GetAlertManager().StopWorker()
h.GetSystemManager().RemoveAllSystems()
h.TestApp.Cleanup()
}
func CreateSystems(app core.App, count int, userId string, status string) ([]*core.Record, error) {
systems := make([]*core.Record, 0, count)
for i := range count {
system, err := CreateRecord(app, "systems", map[string]any{
"name": fmt.Sprintf("test-system-%d", i),
"host": fmt.Sprintf("127.0.0.%d", i),
"port": "33914",
"users": []string{userId},
})
if err != nil {
return nil, err
}
system.Set("status", status)
err = app.SaveNoValidate(system)
if err != nil {
return nil, err
}
systems = append(systems, system)
}
return systems, nil
}

View File

@@ -1,6 +1,8 @@
package migrations package migrations
import ( import (
"os"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations" m "github.com/pocketbase/pocketbase/migrations"
) )
@@ -19,11 +21,51 @@ func init() {
if err := app.Save(settings); err != nil { if err := app.Save(settings); err != nil {
return err return err
} }
// create superuser // create superuser
collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) superuserCollection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
user := core.NewRecord(collection) superUser := core.NewRecord(superuserCollection)
user.SetEmail(TempAdminEmail)
user.SetRandomPassword() // set email
return app.Save(user) email, _ := GetEnv("USER_EMAIL")
password, _ := GetEnv("USER_PASSWORD")
didProvideUserDetails := email != "" && password != ""
// set superuser email
if email == "" {
email = TempAdminEmail
}
superUser.SetEmail(email)
// set superuser password
if password != "" {
superUser.SetPassword(password)
} else {
superUser.SetRandomPassword()
}
// if user details are provided, we create a regular user as well
if didProvideUserDetails {
usersCollection, _ := app.FindCollectionByNameOrId("users")
user := core.NewRecord(usersCollection)
user.SetEmail(email)
user.SetPassword(password)
user.SetVerified(true)
err := app.Save(user)
if err != nil {
return err
}
}
return app.Save(superUser)
}, nil) }, nil)
} }
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
func GetEnv(key string) (value string, exists bool) {
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
return value, exists
}
// Fallback to the old unprefixed key
return os.LookupEnv(key)
}

Binary file not shown.

View File

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

View File

@@ -1,10 +1,10 @@
{ {
"name": "beszel", "name": "beszel",
"private": true, "private": true,
"version": "0.12.4", "version": "0.12.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"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",
@@ -33,6 +33,7 @@
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@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",

View File

@@ -13,8 +13,9 @@ 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, pb } from "@/lib/stores" import { $publicKey } from "@/lib/stores"
import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils" import { cn, generateToken, tokenMap, useBrowserStorage } 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"
@@ -76,7 +77,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
const port = useRef<HTMLInputElement>(null) const port = useRef<HTMLInputElement>(null)
const [hostValue, setHostValue] = useState(system?.host ?? "") const [hostValue, setHostValue] = useState(system?.host ?? "")
const isUnixSocket = hostValue.startsWith("/") const isUnixSocket = hostValue.startsWith("/")
const [tab, setTab] = useLocalStorage("as-tab", "docker") const [tab, setTab] = useBrowserStorage("as-tab", "docker")
const [token, setToken] = useState(system?.token ?? "") const [token, setToken] = useState(system?.token ?? "")
useEffect(() => { useEffect(() => {
@@ -96,7 +97,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
tokenMap.set(system.id, token) tokenMap.set(system.id, token)
setToken(token) setToken(token)
})() })()
}, [system?.id]) }, [system?.id, nextSystemToken])
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault() e.preventDefault()
@@ -132,7 +133,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"> <DialogTitle className="mb-2 max-w-100 truncate pr-8">
{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">

View File

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

@@ -1,6 +1,6 @@
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, pb } from "@/lib/stores" import { $alerts, $systems } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils" import { cn, debounce } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts" import { alertInfo } from "@/lib/alerts"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
@@ -15,6 +15,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ServerIcon, GlobeIcon } from "lucide-react" import { 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"))
@@ -95,7 +96,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" />
{system.name} <span className="truncate max-w-60">{system.name}</span>
</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

@@ -1,13 +1,14 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils" import { cn, formatShortDate, chartMargin } from "@/lib/utils"
import { useYAxisWidth } from "./hooks"
import { ChartData, SystemStatsRecord } from "@/types" import { ChartData, SystemStatsRecord } from "@/types"
import { useMemo } from "react" 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: string color: number | string
opacity: number opacity: number
} }

View File

@@ -1,23 +1,26 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils" import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
// import Spinner from '../spinner' // import Spinner from '../spinner'
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { $containerFilter, $userSettings } from "@/lib/stores" import { $containerFilter, $userSettings } from "@/lib/stores"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { ChartType, Unit } from "@/lib/enums" import { ChartType, Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function ContainerChart({ export default memo(function ContainerChart({
dataKey, dataKey,
chartData, chartData,
chartType, chartType,
chartConfig,
unit = "%", unit = "%",
}: { }: {
dataKey: string dataKey: string
chartData: ChartData chartData: ChartData
chartType: ChartType chartType: ChartType
chartConfig: ChartConfig
unit?: string unit?: string
}) { }) {
const filter = useStore($containerFilter) const filter = useStore($containerFilter)
@@ -28,40 +31,6 @@ export default memo(function ContainerChart({
const isNetChart = chartType === ChartType.Network const isNetChart = chartType === ChartType.Network
const chartConfig = useMemo(() => {
const config = {} as Record<string, { label: string; color: string }>
const totalUsage = new Map<string, number>()
// calculate total usage of each container
for (const stats of containerData) {
for (const key in stats) {
if (!key || key === "created") continue
const currentTotal = totalUsage.get(key) ?? 0
const increment = isNetChart
? (stats[key]?.nr ?? 0) + (stats[key]?.ns ?? 0)
: // @ts-ignore
stats[key]?.[dataKey] ?? 0
totalUsage.set(key, currentTotal + increment)
}
}
// Sort keys and generate colors based on usage
const sortedEntries = Array.from(totalUsage.entries()).sort(([, a], [, b]) => b - a)
const length = sortedEntries.length
sortedEntries.forEach(([key], i) => {
const hue = ((i * 360) / length) % 360
config[key] = {
label: key,
color: `hsl(${hue}, 60%, 55%)`,
}
})
return config satisfies ChartConfig
}, [chartData])
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => { const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
const obj = {} as { const obj = {} as {
toolTipFormatter: (item: any, key: string) => React.ReactNode | string toolTipFormatter: (item: any, key: string) => React.ReactNode | string
@@ -119,7 +88,14 @@ export default memo(function ContainerChart({
return obj return obj
}, []) }, [])
const filterLower = filter?.toLowerCase() // Filter with set lookup
const filteredKeys = useMemo(() => {
if (!filter) {
return new Set<string>()
}
const filterLower = filter.toLowerCase()
return new Set(Object.keys(chartConfig).filter((key) => !key.toLowerCase().includes(filterLower)))
}, [chartConfig, filter])
// console.log('rendered at', new Date()) // console.log('rendered at', new Date())
@@ -162,9 +138,9 @@ export default memo(function ContainerChart({
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />} content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
/> />
{Object.keys(chartConfig).map((key) => { {Object.keys(chartConfig).map((key) => {
const filtered = filterLower && !key.toLowerCase().includes(filterLower) const filtered = filteredKeys.has(key)
let fillOpacity = filtered ? 0.05 : 0.4 const fillOpacity = filtered ? 0.05 : 0.4
let strokeOpacity = filtered ? 0.1 : 1 const strokeOpacity = filtered ? 0.1 : 1
return ( return (
<Area <Area
key={key} key={key}

View File

@@ -1,10 +1,11 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils" import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo } from "react" import { memo } from "react"
import { useLingui } from "@lingui/react/macro" import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums" import { Unit } from "@/lib/enums"
import { useYAxisWidth } from "./hooks"
export default memo(function DiskChart({ export default memo(function DiskChart({
dataKey, dataKey,

View File

@@ -8,9 +8,10 @@ import {
ChartTooltipContent, ChartTooltipContent,
xAxis, xAxis,
} from "@/components/ui/chart" } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils" import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { useYAxisWidth } from "./hooks"
export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) { export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -0,0 +1,107 @@
import { useMemo, useState } from "react"
import { ChartConfig } from "@/components/ui/chart"
import { ChartData } from "@/types"
/** Chart configurations for CPU, memory, and network usage charts */
export interface ContainerChartConfigs {
cpu: ChartConfig
memory: ChartConfig
network: ChartConfig
}
/**
* Generates chart configurations for container metrics visualization
* @param containerData - Array of container statistics data points
* @returns Chart configurations for CPU, memory, and network metrics
*/
export function useContainerChartConfigs(containerData: ChartData["containerData"]): ContainerChartConfigs {
return useMemo(() => {
const configs = {
cpu: {} as ChartConfig,
memory: {} as ChartConfig,
network: {} as ChartConfig,
}
// Aggregate usage metrics for each container
const totalUsage = {
cpu: new Map<string, number>(),
memory: new Map<string, number>(),
network: new Map<string, number>(),
}
// Process each data point to calculate totals
for (let i = 0; i < containerData.length; i++) {
const stats = containerData[i]
const containerNames = Object.keys(stats)
for (let j = 0; j < containerNames.length; j++) {
const containerName = containerNames[j]
// Skip metadata field
if (containerName === "created") {
continue
}
const containerStats = stats[containerName]
if (!containerStats) {
continue
}
// Accumulate metrics for CPU, memory, and network
const currentCpu = totalUsage.cpu.get(containerName) ?? 0
const currentMemory = totalUsage.memory.get(containerName) ?? 0
const currentNetwork = totalUsage.network.get(containerName) ?? 0
totalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))
totalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))
totalUsage.network.set(containerName, currentNetwork + (containerStats.nr ?? 0) + (containerStats.ns ?? 0))
}
}
// Generate chart configurations for each metric type
Object.entries(totalUsage).forEach(([chartType, usageMap]) => {
const sortedContainers = Array.from(usageMap.entries()).sort(([, a], [, b]) => b - a)
const chartConfig = {} as Record<string, { label: string; color: string }>
const count = sortedContainers.length
// Generate colors for each container
for (let i = 0; i < count; i++) {
const [containerName] = sortedContainers[i]
const hue = ((i * 360) / count) % 360
chartConfig[containerName] = {
label: containerName,
color: `hsl(${hue}, 60%, 55%)`,
}
}
configs[chartType as keyof typeof configs] = chartConfig
})
return configs
}, [containerData])
}
/** Sets the correct width of the y axis in recharts based on the longest label */
export function useYAxisWidth() {
const [yAxisWidth, setYAxisWidth] = useState(0)
let maxChars = 0
let timeout: ReturnType<typeof setTimeout>
function updateYAxisWidth(str: string) {
if (str.length > maxChars) {
maxChars = str.length
const div = document.createElement("div")
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
const width = div.offsetWidth + 24
if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24)
}
document.body.removeChild(div)
})
}
return str
}
return { yAxisWidth, updateYAxisWidth }
}

View File

@@ -8,10 +8,11 @@ import {
ChartTooltipContent, ChartTooltipContent,
xAxis, xAxis,
} from "@/components/ui/chart" } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils" import { cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData, SystemStats } from "@/types" import { ChartData, SystemStats } from "@/types"
import { memo } from "react" import { memo } from "react"
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import { useYAxisWidth } from "./hooks"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) { export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -1,12 +1,13 @@
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { useYAxisWidth, cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils" import { cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { memo } from "react" import { memo } from "react"
import { ChartData } from "@/types" 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"
import { useYAxisWidth } from "./hooks"
export default memo(function MemChart({ chartData }: { chartData: ChartData }) { export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { t } = useLingui() const { t } = useLingui()
@@ -66,7 +67,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
<Area <Area
name={t`Used`} name={t`Used`}
order={3} order={3}
dataKey="stats.mu" dataKey={({ stats }) => (showMax ? stats?.mm : stats?.mu)}
type="monotoneX" type="monotoneX"
fill="var(--chart-2)" fill="var(--chart-2)"
fillOpacity={0.4} fillOpacity={0.4}
@@ -74,31 +75,31 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
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.mz" dataKey={({ stats }) => (showMax ? null : 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.mb" dataKey={({ stats }) => (showMax ? null : 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

@@ -2,11 +2,12 @@ import { t } from "@lingui/core/macro"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts" import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart" import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils" import { cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo } from "react" import { memo } from "react"
import { $userSettings } from "@/lib/stores" import { $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { useYAxisWidth } from "./hooks"
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) { export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()

View File

@@ -8,19 +8,12 @@ import {
ChartTooltipContent, ChartTooltipContent,
xAxis, xAxis,
} from "@/components/ui/chart" } from "@/components/ui/chart"
import { import { cn, formatShortDate, toFixedFloat, chartMargin, formatTemperature, decimalString } from "@/lib/utils"
useYAxisWidth,
cn,
formatShortDate,
toFixedFloat,
chartMargin,
formatTemperature,
decimalString,
} from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData } from "@/types"
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { $temperatureFilter, $userSettings } from "@/lib/stores" import { $temperatureFilter, $userSettings } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { useYAxisWidth } from "./hooks"
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) { export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
const filter = useStore($temperatureFilter) const filter = useStore($temperatureFilter)

View File

@@ -23,11 +23,13 @@ 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, isAdmin, listen } from "@/lib/utils" import { getHostDisplayValue, 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(() => {
@@ -54,11 +56,9 @@ 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>{system.name}</span> <span className="max-w-60 truncate">{system.name}</span>
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut> <CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
</CommandItem> </CommandItem>
))} ))}
@@ -121,6 +121,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
{SettingsShortcut} {SettingsShortcut}
</CommandItem> </CommandItem>
<CommandItem <CommandItem
keywords={[t`Universal token`]}
onSelect={() => { onSelect={() => {
navigate(getPagePath($router, "settings", { name: "tokens" })) navigate(getPagePath($router, "settings", { name: "tokens" }))
setOpen(false) setOpen(false)
@@ -214,6 +215,9 @@ 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", lang === i18n.locale && "font-semibold")} className={cn("px-2.5 flex gap-2.5 cursor-pointer", lang === i18n.locale && "bg-accent/70 font-medium")}
onClick={() => dynamicActivate(lang)} onClick={() => dynamicActivate(lang)}
> >
<span>{e}</span> {label} <span>{e}</span> {label}

View File

@@ -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, pb } from "@/lib/stores" import { $authenticated } 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,6 +13,7 @@ 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.`))

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,13 +1,14 @@
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)
@@ -50,8 +51,11 @@ 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" ? "30 8% 80%" : "220 3% 20%" }} style={{ maxWidth: "22em", "--border": theme == "light" ? "hsl(30, 8%, 70%)" : "hsl(220, 3%, 25%)" }}
> >
<div className="absolute top-3 right-3">
<ModeToggle />
</div>
<div className="text-center"> <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

@@ -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 { pb } from "@/lib/stores" import { cn, runOnce } from "@/lib/utils"
import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils" import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -36,12 +36,17 @@ 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 href={basePath} aria-label="Home" className="p-2 ps-0 me-3"> <Link
href={basePath}
aria-label="Home"
className="p-2 ps-0 me-3"
onMouseEnter={runOnce(() => import("@/components/routes/home"))}
>
<Logo className="h-[1.1rem] md:h-5 fill-foreground" /> <Logo className="h-[1.1rem] md:h-5 fill-foreground" />
</Link> </Link>
<SearchButton /> <SearchButton />
<div className="flex items-center ms-auto"> <div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
<LangToggle /> <LangToggle />
<ModeToggle /> <ModeToggle />
<Link <Link

View File

@@ -1,18 +1,16 @@
import { Suspense, lazy, memo, useEffect, useMemo } from "react" import { Suspense, memo, useEffect, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $systems, pb } from "@/lib/stores" import { $alerts, $allSystemsById } 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, updateRecordList, updateSystemList } from "@/lib/utils" import { AlertRecord } 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 { alertInfo } from "@/lib/alerts"
import SystemsTable from "@/components/systems-table/systems-table"
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default memo(function () { export default memo(function () {
const { t } = useLingui() const { t } = useLingui()
@@ -21,19 +19,6 @@ export default memo(function () {
document.title = t`Dashboard` + " / Beszel" document.title = t`Dashboard` + " / Beszel"
}, [t]) }, [t])
useEffect(() => {
// make sure we have the latest list of systems
updateSystemList()
// subscribe to real time updates for systems / alerts
pb.collection<SystemRecord>("systems").subscribe("*", (e) => {
updateRecordList(e, $systems)
})
return () => {
pb.collection("systems").unsubscribe("*")
}
}, [])
return useMemo( return useMemo(
() => ( () => (
<> <>
@@ -42,7 +27,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 text-xs opacity-80"> <div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
<a <a
href="https://github.com/henrygd/beszel" href="https://github.com/henrygd/beszel"
target="_blank" target="_blank"
@@ -67,6 +52,7 @@ export default memo(function () {
const ActiveAlerts = () => { const ActiveAlerts = () => {
const alerts = useStore($alerts) const alerts = useStore($alerts)
const systems = useStore($allSystemsById)
const { activeAlerts, alertsKey } = useMemo(() => { const { activeAlerts, alertsKey } = useMemo(() => {
const activeAlerts: AlertRecord[] = [] const activeAlerts: AlertRecord[] = []
@@ -110,7 +96,7 @@ const ActiveAlerts = () => {
> >
<info.icon className="h-4 w-4" /> <info.icon className="h-4 w-4" />
<AlertTitle> <AlertTitle>
{getSystemNameFromId(alert.system)} {info.name().toLowerCase().replace("cpu", "CPU")} {systems[alert.system]?.name} {info.name().toLowerCase().replace("cpu", "CPU")}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
{alert.name === "Status" ? ( {alert.name === "Status" ? (
@@ -123,7 +109,7 @@ const ActiveAlerts = () => {
)} )}
</AlertDescription> </AlertDescription>
<Link <Link
href={getPagePath($router, "system", { name: getSystemNameFromId(alert.system) })} href={getPagePath($router, "system", { name: systems[alert.system]?.name })}
className="absolute inset-0 w-full h-full" className="absolute inset-0 w-full h-full"
aria-label="View system" aria-label="View system"
></Link> ></Link>

View File

@@ -1,4 +1,4 @@
import { pb } from "@/lib/stores" import { pb } from "@/lib/api"
import { cn, formatDuration, formatShortDate } from "@/lib/utils" import { cn, formatDuration, formatShortDate } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts" import { alertInfo } from "@/lib/alerts"
import { AlertsHistoryRecord } from "@/types" import { AlertsHistoryRecord } from "@/types"
@@ -273,13 +273,13 @@ export default function AlertsHistoryDataTable() {
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <tr key={headerGroup.id} className="border-border/50">
{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>
))} ))}
</TableRow> </tr>
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>

View File

@@ -1,17 +1,16 @@
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

@@ -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 { useEffect } from "react" import { lazy, 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,15 +8,23 @@ 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, pb } from "@/lib/stores.ts" import { $userSettings } 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 Fingerprints from "./tokens-fingerprints.tsx" import { pb } from "@/lib/api"
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 {
@@ -59,23 +67,27 @@ 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,
}, },
] ]
@@ -90,7 +102,7 @@ export default function SettingsLayout() {
}, []) }, [])
return ( return (
<Card className="pt-5 px-4 pb-8 min-h-96 sm:pt-6 sm:px-7"> <Card className="pt-5 px-4 pb-8 min-h-96 mb-14 sm:pt-6 sm:px-7">
<CardHeader className="p-0"> <CardHeader className="p-0">
<CardTitle className="mb-1"> <CardTitle className="mb-1">
<Trans>Settings</Trans> <Trans>Settings</Trans>
@@ -120,14 +132,14 @@ function SettingsContent({ name }: { name: string }) {
switch (name) { switch (name) {
case "general": case "general":
return <General userSettings={userSettings} /> return <GeneralSettings userSettings={userSettings} />
case "notifications": case "notifications":
return <Notifications userSettings={userSettings} /> return <NotificationsSettings userSettings={userSettings} />
case "config": case "config":
return <ConfigYaml /> return <ConfigYamlSettings />
case "tokens": case "tokens":
return <Fingerprints /> return <FingerprintsSettings />
case "alert-history": case "alert-history":
return <AlertsHistoryDataTable /> return <AlertsHistoryDataTableSettings />
} }
} }

View File

@@ -3,7 +3,6 @@ import { Trans } from "@lingui/react/macro"
import { Button } from "@/components/ui/button" import { 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"
@@ -13,8 +12,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

View File

@@ -1,5 +1,6 @@
import React from "react" import React from "react"
import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils" import { cn } from "@/lib/utils"
import { isAdmin, isReadOnlyUser } from "@/lib/api"
import { buttonVariants } from "../../ui/button" import { 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"
@@ -13,6 +14,7 @@ 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> }>
}[] }[]
} }
@@ -52,6 +54,7 @@ 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, pb } from "@/lib/stores" import { $publicKey } 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,7 +14,8 @@ 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, isReadOnlyUser, tokenMap } from "@/lib/utils" import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
import { isReadOnlyUser, pb } from "@/lib/api"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -271,7 +272,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>
<TableRow> <tr className="border-border/50">
{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">
@@ -287,12 +288,14 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
</span> </span>
</TableHead> </TableHead>
)} )}
</TableRow> </tr>
</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">{fingerprint.expand.system.name}</TableCell> <TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
{fingerprint.expand.system.name}
</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.token}</TableCell> <TableCell className="font-mono text-[0.95em] py-2">{fingerprint.token}</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.fingerprint}</TableCell> <TableCell className="font-mono text-[0.95em] py-2">{fingerprint.fingerprint}</TableCell>
{!isReadOnly && ( {!isReadOnly && (

View File

@@ -2,17 +2,18 @@ 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,
$direction, $direction,
$maxValues, $maxValues,
$temperatureFilter, $temperatureFilter,
$allSystemsByName,
} 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 { useContainerChartConfigs } from "@/components/charts/hooks"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums" import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react" import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card" import { 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"
@@ -24,12 +25,12 @@ import {
decimalString, decimalString,
formatBytes, formatBytes,
getHostDisplayValue, getHostDisplayValue,
getPbTimestamp,
listen, listen,
parseSemVer, parseSemVer,
toFixedFloat, toFixedFloat,
useLocalStorage, useBrowserStorage,
} 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"
@@ -42,15 +43,15 @@ 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 { 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")) import { subscribeKeys } from "nanostores"
const cache = new Map<string, any>() const cache = new Map<string, any>()
@@ -119,13 +120,13 @@ function dockerOrPodman(str: string, system: SystemRecord) {
return str return str
} }
export default function SystemDetail({ name }: { name: string }) { export default memo(function SystemDetail({ name }: { name: string }) {
const direction = useStore($direction) const direction = useStore($direction)
const { t } = useLingui() const { t } = useLingui()
const systems = useStore($systems) const systems = useStore($systems)
const chartTime = useStore($chartTime) const chartTime = useStore($chartTime)
const maxValues = useStore($maxValues) const maxValues = useStore($maxValues)
const [grid, setGrid] = useLocalStorage("grid", true) const [grid, setGrid] = useBrowserStorage("grid", true)
const [system, setSystem] = useState({} as SystemRecord) const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[]) const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"]) const [containerData, setContainerData] = useState([] as ChartData["containerData"])
@@ -151,36 +152,13 @@ export default function SystemDetail({ name }: { name: string }) {
} }
}, [name]) }, [name])
// function resetCharts() { // find matching system and update when it changes
// setSystemStats([])
// setContainerData([])
// }
// useEffect(resetCharts, [chartTime])
// find matching system
useEffect(() => { useEffect(() => {
if (system.id && system.name === name) { return subscribeKeys($allSystemsByName, [name], (newSystems) => {
return const sys = newSystems[name]
} sys?.id && setSystem(sys)
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
if (matchingSystem) {
setSystem(matchingSystem)
}
}, [name, system, systems])
// update system when new data is available
useEffect(() => {
if (!system.id) {
return
}
pb.collection<SystemRecord>("systems").subscribe(system.id, (e) => {
setSystem(e.record)
}) })
return () => { }, [name])
pb.collection("systems").unsubscribe(system.id)
}
}, [system.id])
const chartData: ChartData = useMemo(() => { const chartData: ChartData = useMemo(() => {
const lastCreated = Math.max( const lastCreated = Math.max(
@@ -197,6 +175,9 @@ export default function SystemDetail({ name }: { name: string }) {
} }
}, [systemStats, containerData, direction]) }, [systemStats, containerData, direction])
// Share chart config computation for all container charts
const containerChartConfigs = useContainerChartConfigs(containerData)
// get stats // get stats
useEffect(() => { useEffect(() => {
if (!system.id || !chartTime) { if (!system.id || !chartTime) {
@@ -287,11 +268,19 @@ 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 < 172800) { if (system.info.u < 3600) {
const hours = Math.trunc(system.info.u / 3600) uptime = (
uptime = <Plural value={hours} one="# hour" other="# hours" /> <Plural
value={Math.trunc(system.info.u / 60)}
one="# minute"
few="# minutes"
many="# minutes"
other="# minutes"
/>
)
} else if (system.info.u < 172800) {
uptime = <Plural value={Math.trunc(system.info.u / 3600)} one="# hour" other="# hours" />
} else { } else {
uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" /> uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" />
} }
@@ -317,7 +306,7 @@ export default function SystemDetail({ name }: { name: string }) {
Icon: any Icon: any
hide?: boolean hide?: boolean
}[] }[]
}, [system.info]) }, [system.info, t])
/** Space for tooltip if more than 12 containers */ /** Space for tooltip if more than 12 containers */
useEffect(() => { useEffect(() => {
@@ -391,7 +380,7 @@ export default function SystemDetail({ name }: { name: string }) {
return ( return (
<> <>
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip"> <div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip">
{/* system info */} {/* 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">
@@ -486,7 +475,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,
}, },
]} ]}
@@ -503,7 +492,12 @@ export default function SystemDetail({ name }: { name: string }) {
description={t`Average CPU utilization of containers`} description={t`Average CPU utilization of containers`}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
<ContainerChart chartData={chartData} dataKey="c" chartType={ChartType.CPU} /> <ContainerChart
chartData={chartData}
dataKey="c"
chartType={ChartType.CPU}
chartConfig={containerChartConfigs.cpu}
/>
</ChartCard> </ChartCard>
)} )}
@@ -512,8 +506,9 @@ 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} /> <MemChart chartData={chartData} showMax={showMax} />
</ChartCard> </ChartCard>
{containerFilterBar && ( {containerFilterBar && (
@@ -524,7 +519,12 @@ export default function SystemDetail({ name }: { name: string }) {
description={dockerOrPodman(t`Memory usage of docker containers`, system)} description={dockerOrPodman(t`Memory usage of docker containers`, system)}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
<ContainerChart chartData={chartData} dataKey="m" chartType={ChartType.Memory} /> <ContainerChart
chartData={chartData}
dataKey="m"
chartType={ChartType.Memory}
chartConfig={containerChartConfigs.memory}
/>
</ChartCard> </ChartCard>
)} )}
@@ -546,13 +546,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,
}, },
]} ]}
@@ -587,7 +587,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,
}, },
{ {
@@ -598,7 +598,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,
}, },
]} ]}
@@ -626,8 +626,12 @@ export default function SystemDetail({ name }: { name: string }) {
description={dockerOrPodman(t`Network traffic of docker containers`, system)} description={dockerOrPodman(t`Network traffic of docker containers`, system)}
cornerEl={containerFilterBar} cornerEl={containerFilterBar}
> >
{/* @ts-ignore */} <ContainerChart
<ContainerChart chartData={chartData} chartType={ChartType.Network} dataKey="n" /> chartData={chartData}
chartType={ChartType.Network}
dataKey="n"
chartConfig={containerChartConfigs.network}
/>
</ChartCard> </ChartCard>
</div> </div>
)} )}
@@ -687,7 +691,7 @@ export default function SystemDetail({ name }: { name: string }) {
{ {
label: t`Charge`, label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0], dataKey: ({ stats }) => stats?.bat?.[0],
color: "1", color: 1,
opacity: 0.35, opacity: 0.35,
}, },
]} ]}
@@ -730,7 +734,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,
}, },
]} ]}
@@ -750,7 +754,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,
}, },
]} ]}
@@ -802,13 +806,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,
}, },
]} ]}
@@ -834,7 +838,7 @@ export default function SystemDetail({ name }: { name: string }) {
{bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />} {bottomSpacing > 0 && <span className="block" style={{ height: bottomSpacing }} />}
</> </>
) )
} })
function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) { function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {
const containerFilter = useStore(store) const containerFilter = useStore(store)

View File

@@ -23,12 +23,11 @@ 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 { $userSettings, pb } from "@/lib/stores" import { $longestSystemNameLen, $userSettings } 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"
@@ -57,6 +56,7 @@ import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums" import { MeterState, SystemStatus } from "@/lib/enums"
import { $router, Link } from "../router" import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { isReadOnlyUser, pb } from "@/lib/api"
const STATUS_COLORS = { const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500", [SystemStatus.Up]: "bg-green-500",
@@ -72,7 +72,8 @@ 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",
@@ -111,11 +112,15 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
Icon: ServerIcon, Icon: ServerIcon,
cell: (info) => { cell: (info) => {
const { name } = info.row.original const { name } = info.row.original
const longestName = useStore($longestSystemNameLen)
return ( return (
<> <>
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5"> <span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
<IndicatorDot system={info.row.original} /> <IndicatorDot system={info.row.original} />
{name} {/* NOTE: change to 1 ch if switching to monospace font */}
<span className="truncate" style={{ width: `${longestName / 1.1}ch` }}>
{name}
</span>
</span> </span>
<Link <Link
href={getPagePath($router, "system", { name })} href={getPagePath($router, "system", { name })}
@@ -318,22 +323,18 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) { function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0 const val = Number(info.getValue()) || 0
const threshold = getMeterState(val) const threshold = getMeterState(val)
const meterClass = cn(
"h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)
return ( return (
<div className="flex gap-2 items-center tabular-nums tracking-tight"> <div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span> <span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden"> <span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span <span className={meterClass} style={{ width: `${val}%` }}></span>
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
</span> </span>
</div> </div>
) )

View File

@@ -11,11 +11,8 @@ 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,
@@ -36,33 +33,65 @@ import {
ArrowUpIcon, ArrowUpIcon,
Settings2Icon, Settings2Icon,
EyeIcon, EyeIcon,
FilterIcon,
} from "lucide-react" } from "lucide-react"
import { memo, useEffect, useMemo, useState } from "react" import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $systems } from "@/lib/stores" import { $pausedSystems, $downSystems, $upSystems, $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, useLocalStorage } from "@/lib/utils" import { cn, runOnce, useBrowserStorage } from "@/lib/utils"
import { $router, Link } from "../router" import { $router, Link } 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 "../ui/input" import { Input } from "@/components/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 { SystemStatus } from "@/lib/enums"
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
type ViewMode = "table" | "grid" type ViewMode = "table" | "grid"
type StatusFilter = "all" | SystemRecord["status"]
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 downSystems = $downSystems.get()
const upSystems = $upSystems.get()
const pausedSystems = $pausedSystems.get()
const { i18n, t } = useLingui() const { i18n, t } = useLingui()
const [filter, setFilter] = useState<string>() const [filter, setFilter] = useState<string>()
const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }]) const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [sorting, setSorting] = useBrowserStorage<SortingState>(
"sortMode",
[{ id: "system", desc: false }],
sessionStorage
)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {}) const [columnVisibility, setColumnVisibility] = useBrowserStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
const locale = i18n.locale const locale = i18n.locale
// Filter data based on status filter
const filteredData = useMemo(() => {
if (statusFilter === "all") {
return data
}
if (statusFilter === SystemStatus.Up) {
return Object.values(upSystems) ?? []
}
if (statusFilter === SystemStatus.Down) {
return Object.values(downSystems) ?? []
}
return Object.values(pausedSystems) ?? []
}, [data, statusFilter])
const [viewMode, setViewMode] = useBrowserStorage<ViewMode>(
"viewMode",
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table"
)
useEffect(() => { useEffect(() => {
if (filter !== undefined) { if (filter !== undefined) {
table.getColumn("system")?.setFilterValue(filter) table.getColumn("system")?.setFilterValue(filter)
@@ -72,7 +101,7 @@ export default function SystemsTable() {
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode]) const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode])
const table = useReactTable({ const table = useReactTable({
data, data: filteredData,
columns: columnDefs, columns: columnDefs,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting, onSortingChange: setSorting,
@@ -86,7 +115,6 @@ export default function SystemsTable() {
columnVisibility, columnVisibility,
}, },
defaultColumn: { defaultColumn: {
// sortDescFirst: true,
invertSorting: true, invertSorting: true,
sortUndefined: "last", sortUndefined: "last",
minSize: 0, minSize: 0,
@@ -98,19 +126,25 @@ export default function SystemsTable() {
const rows = table.getRowModel().rows const rows = table.getRowModel().rows
const columns = table.getAllColumns() const columns = table.getAllColumns()
const visibleColumns = table.getVisibleLeafColumns() const visibleColumns = table.getVisibleLeafColumns()
const [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => {
return [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length]
}, [upSystems, downSystems, pausedSystems])
// TODO: hiding temp then gpu messes up table headers // TODO: hiding temp then gpu messes up table headers
const CardHead = useMemo(() => { const CardHead = useMemo(() => {
return ( return (
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1"> <CardHeader className="pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-5 w-full items-end"> <div className="grid md:flex gap-5 w-full items-end">
<div className="px-2 sm:px-1"> <div className="px-2 sm:px-1">
<CardTitle className="mb-2.5"> <CardTitle className="mb-2">
<Trans>All Systems</Trans> <Trans>All Systems</Trans>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="flex">
<Trans>Updated in real time. Click on a system to view information.</Trans> <Trans>Click on a system to view more information.</Trans>
</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>
@@ -121,8 +155,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-3 divide-y md:divide-s md:divide-y-0"> <div className="grid grid-cols-1 md:grid-cols-4 divide-y md:divide-s md:divide-y-0">
<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">
<LayoutGridIcon className="size-4" /> <LayoutGridIcon className="size-4" />
<Trans>Layout</Trans> <Trans>Layout</Trans>
@@ -144,7 +178,33 @@ export default function SystemsTable() {
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
</div> </div>
<div> <div className="border-r">
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<FilterIcon className="size-4" />
<Trans>Status</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
className="px-1 pb-1"
value={statusFilter}
onValueChange={(value) => setStatusFilter(value as StatusFilter)}
>
<DropdownMenuRadioItem value="all" onSelect={(e) => e.preventDefault()}>
<Trans>All Systems</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
<Trans>Up ({upSystemsLength})</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
<Trans>Down ({downSystemsLength})</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
<Trans>Paused ({pausedSystemsLength})</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>
@@ -210,7 +270,16 @@ export default function SystemsTable() {
</div> </div>
</CardHeader> </CardHeader>
) )
}, [visibleColumns.length, sorting, viewMode, locale]) }, [
visibleColumns.length,
sorting,
viewMode,
locale,
statusFilter,
upSystemsLength,
downSystemsLength,
pausedSystemsLength,
])
return ( return (
<Card> <Card>
@@ -218,7 +287,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 border overflow-hidden"> <div className="rounded-md">
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} /> <AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
</div> </div>
) : ( ) : (
@@ -240,36 +309,78 @@ export default function SystemsTable() {
) )
} }
const AllSystemsTable = memo( const AllSystemsTable = memo(function ({
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => { table,
return ( rows,
<Table> colLength,
<SystemsTableHead table={table} colLength={colLength} /> }: {
<TableBody> table: TableType<SystemRecord>
{rows.length ? ( rows: Row<SystemRecord>[]
rows.map((row) => ( colLength: number
<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>
<TableCell colSpan={colLength} className="h-24 text-center"> const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
<Trans>No systems found.</Trans> count: rows.length,
</TableCell> estimateSize: () => (rows.length > 10 ? 56 : 60),
</TableRow> getScrollElement: () => scrollRef.current,
)} overscan: 5,
</TableBody> })
</Table> const virtualRows = virtualizer.getVirtualItems()
)
} const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
) const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full">
<SystemsTableHead table={table} colLength={colLength} />
<TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<SystemRecord>
return (
<SystemTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
})
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) { function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
const { i18n } = useLingui() const { i18n } = useLingui()
return useMemo(() => { return useMemo(() => {
return ( return (
<TableHeader> <TableHeader className="sticky top-0 z-20 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <tr 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}>
@@ -277,41 +388,49 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
</TableHead> </TableHead>
) )
})} })}
</TableRow> </tr>
))} ))}
</TableHeader> </TableHeader>
) )
}, [i18n.locale, colLength]) }, [i18n.locale, colLength])
} }
const SystemTableRow = memo( const SystemTableRow = memo(function ({
({ row, length, colLength }: { row: Row<SystemRecord>; length: number; colLength: number }) => { row,
const system = row.original virtualRow,
const { t } = useLingui() colLength,
return useMemo(() => { }: {
return ( row: Row<SystemRecord>
<TableRow virtualRow: VirtualItem
// data-state={row.getIsSelected() && "selected"} length: number
className={cn("cursor-pointer transition-opacity relative", { colLength: number
"opacity-50": system.status === SystemStatus.Paused, }) {
})} const system = row.original
> const { t } = useLingui()
{row.getVisibleCells().map((cell) => ( return useMemo(() => {
<TableCell return (
key={cell.id} <TableRow
style={{ // data-state={row.getIsSelected() && "selected"}
width: cell.column.getSize(), className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
}} "opacity-50": system.status === SystemStatus.Paused,
className={length > 10 ? "py-2" : "py-2.5"} })}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {row.getVisibleCells().map((cell) => (
</TableCell> <TableCell
))} key={cell.id}
</TableRow> style={{
) width: cell.column.getSize(),
}, [system, system.status, colLength, t]) height: virtualRow.size,
} }}
) className="py-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</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 }) => {
@@ -321,6 +440,7 @@ 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",
@@ -330,13 +450,11 @@ const SystemCard = memo(
)} )}
> >
<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 justify-between gap-2"> <div className="flex items-center gap-2 w-full overflow-hidden">
<CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-h-10 gap-2.5 min-w-0"> <CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5">
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0 flex-1">
<IndicatorDot system={system} /> <IndicatorDot system={system} />
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90"> <span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
{system.name}
</CardTitle>
</div> </div>
</CardTitle> </CardTitle>
{table.getColumn("actions")?.getIsVisible() && ( {table.getColumn("actions")?.getIsVisible() && (
@@ -347,23 +465,33 @@ const SystemCard = memo(
)} )}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-2.5 text-sm px-5 pt-3.5 pb-4"> <CardContent className="text-sm px-5 pt-3.5 pb-4">
{table.getAllColumns().map((column) => { <div className="grid gap-2.5" style={{ gridTemplateColumns: "24px minmax(80px, max-content) 1fr" }}>
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null {table.getAllColumns().map((column) => {
const cell = row.getAllCells().find((cell) => cell.column.id === column.id) if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
if (!cell) return null const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
// @ts-ignore if (!cell) return null
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown> // @ts-ignore
return ( const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
<div key={column.id} className="flex items-center gap-3"> return (
{Icon && <Icon className="size-4 text-muted-foreground" />} <>
<div className="flex items-center gap-3 flex-1"> <div key={`${column.id}-icon`} className="flex items-center">
<span className="text-muted-foreground min-w-16">{name()}:</span> {column.id === "lastSeen" ? (
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div> <EyeIcon className="size-4 text-muted-foreground" />
</div> ) : (
</div> Icon && <Icon className="size-4 text-muted-foreground" />
) )}
})} </div>
<div key={`${column.id}-label`} className="flex items-center text-muted-foreground pr-3">
{name()}:
</div>
<div key={`${column.id}-value`} className="flex items-center">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</>
)
})}
</div>
</CardContent> </CardContent>
<Link <Link
href={getPagePath($router, "system", { name: row.original.name })} href={getPagePath($router, "system", { name: row.original.name })}

View File

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

View File

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

View File

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

View File

@@ -2,21 +2,19 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} function Input({ className, type, ...props }: React.ComponentProps<"input">) {
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-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className
)} )}
ref={ref}
{...props} {...props}
/> />
) )
}) }
Input.displayName = "Input"
export { Input } export { Input }

View File

@@ -13,7 +13,11 @@ 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 ref={ref} className={cn("bg-muted/30 [&_tr]:border-b", className)} {...props} /> <thead
ref={ref}
className={cn("bg-table-header border-b border-border/50 [&_tr]:border-b", className)}
{...props}
/>
) )
) )
TableHeader.displayName = "TableHeader" TableHeader.displayName = "TableHeader"

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
import { ChartTimes, UserSettings } from "@/types"
import { $alerts, $allSystemsByName, $userSettings } from "./stores"
import { toast } from "@/components/ui/use-toast"
import { t } from "@lingui/core/macro"
import { chartTimeData } from "./utils"
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"
export 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() {
$allSystemsByName.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)
}
}
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

View File

@@ -1,17 +1,23 @@
import PocketBase from "pocketbase" import { atom, computed, map, ReadableAtom } 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)
/** List of system records */ /** Map of system records by name */
export const $systems = atom<SystemRecord[]>([]) export const $allSystemsByName = map<Record<string, SystemRecord>>({})
/** Map of system records by id */
export const $allSystemsById = map<Record<string, SystemRecord>>({})
/** Map of up systems by id */
export const $upSystems = map<Record<string, SystemRecord>>({})
/** Map of down systems by id */
export const $downSystems = map<Record<string, SystemRecord>>({})
/** Map of paused systems by id */
export const $pausedSystems = map<Record<string, SystemRecord>>({})
/** List of all system records */
export const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsById, Object.values)
/** Map of alert records by system id and alert name */ /** Map of alert records by system id and alert name */
export const $alerts = map<AlertMap>({}) export const $alerts = map<AlertMap>({})
@@ -57,3 +63,8 @@ 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

@@ -0,0 +1,174 @@
import { SystemRecord } from "@/types"
import { PreinitializedMapStore } from "nanostores"
import { pb, verifyAuth } from "@/lib/api"
import {
$allSystemsByName,
$upSystems,
$downSystems,
$pausedSystems,
$allSystemsById,
$longestSystemNameLen,
} from "@/lib/stores"
import { updateFavicon, FAVICON_DEFAULT, FAVICON_GREEN, FAVICON_RED } from "@/lib/utils"
import { SystemStatus } from "./enums"
const COLLECTION = pb.collection<SystemRecord>("systems")
const FIELDS_DEFAULT = "id,name,host,port,info,status"
/** Maximum system name length for display purposes */
const MAX_SYSTEM_NAME_LENGTH = 20
let initialized = false
let unsub: (() => void) | undefined | void
/** Initialize the systems manager and set up listeners */
export function init() {
if (initialized) {
return
}
initialized = true
// sync system stores on change
$allSystemsById.listen((newSystems, oldSystems, changedKey) => {
const oldSystem = oldSystems[changedKey]
const newSystem = newSystems[changedKey]
// if system is undefined (deleted), remove it from the stores
if (oldSystem && !newSystem?.id) {
removeFromStore(oldSystem, $upSystems)
removeFromStore(oldSystem, $downSystems)
removeFromStore(oldSystem, $pausedSystems)
removeFromStore(oldSystem, $allSystemsById)
}
if (!newSystem) {
onSystemsChanged(newSystems, undefined)
return
}
const newStatus = newSystem.status
if (newStatus === SystemStatus.Up) {
$upSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $downSystems)
removeFromStore(newSystem, $pausedSystems)
} else if (newStatus === SystemStatus.Down) {
$downSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $pausedSystems)
} else if (newStatus === SystemStatus.Paused) {
$pausedSystems.setKey(newSystem.id, newSystem)
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $downSystems)
} else if (newStatus === SystemStatus.Pending) {
removeFromStore(newSystem, $upSystems)
removeFromStore(newSystem, $downSystems)
removeFromStore(newSystem, $pausedSystems)
}
// run things that need to be done when systems change
onSystemsChanged(newSystems, newSystem)
})
}
/** Update the longest system name length and favicon based on system status */
function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {
const upSystemsStore = $upSystems.get()
const downSystemsStore = $downSystems.get()
const upSystems = Object.values(upSystemsStore)
const downSystems = Object.values(downSystemsStore)
// Update longest system name length
const longestName = $longestSystemNameLen.get()
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
if (nameLen > longestName) {
$longestSystemNameLen.set(nameLen)
}
// Update favicon based on system status
if (downSystems.length > 0) {
updateFavicon(FAVICON_RED)
} else if (upSystems.length > 0) {
updateFavicon(FAVICON_GREEN)
} else {
updateFavicon(FAVICON_DEFAULT)
}
}
/** Fetch systems from collection */
async function fetchSystems(): Promise<SystemRecord[]> {
try {
return await COLLECTION.getFullList({ sort: "+name", fields: FIELDS_DEFAULT })
} catch (error) {
console.error("Failed to fetch systems:", error)
return []
}
}
/** Add system to both name and ID stores */
export function add(system: SystemRecord) {
$allSystemsByName.setKey(system.name, system)
$allSystemsById.setKey(system.id, system)
}
/** Update system in stores */
export function update(system: SystemRecord) {
// if name changed, make sure old name is removed from the name store
const oldName = $allSystemsById.get()[system.id]?.name
if (oldName !== system.name) {
$allSystemsByName.setKey(oldName, undefined as any)
}
add(system)
}
/** Remove system from stores */
export function remove(system: SystemRecord) {
removeFromStore(system, $allSystemsByName)
removeFromStore(system, $allSystemsById)
removeFromStore(system, $upSystems)
removeFromStore(system, $downSystems)
removeFromStore(system, $pausedSystems)
}
/** Remove system from specific store */
function removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {
const key = store === $allSystemsByName ? system.name : system.id
store.setKey(key, undefined as any)
}
/** Action functions for subscription */
const actionFns: Record<string, (system: SystemRecord) => void> = {
create: add,
update: update,
delete: remove,
}
/** Subscribe to real-time system updates from the collection */
export async function subscribe() {
try {
unsub = await COLLECTION.subscribe("*", ({ action, record }) => actionFns[action]?.(record), {
fields: FIELDS_DEFAULT,
})
} catch (error) {
console.error("Failed to subscribe to systems collection:", error)
}
}
/** Refresh all systems with latest data from the hub */
export async function refresh() {
try {
const records = await fetchSystems()
if (!records.length) {
// No systems found, verify authentication
verifyAuth()
return
}
for (const record of records) {
add(record)
}
} catch (error) {
console.error("Failed to refresh systems:", error)
}
}
/** Unsubscribe from real-time system updates */
export const unsubscribe = () => (unsub = unsub?.())

View File

@@ -2,14 +2,16 @@ 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 { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores" import { $copyContent, $userSettings } from "./stores"
import type { ChartTimeData, ChartTimes, FingerprintRecord, SemVer, SystemRecord, UserSettings } from "@/types" import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } 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 { prependBasePath } from "@/components/router"
import { MeterState, Unit } from "./enums" import { MeterState, Unit } from "./enums"
import { prependBasePath } from "@/components/router"
export const FAVICON_DEFAULT = "favicon.svg"
export const FAVICON_GREEN = "favicon-green.svg"
export const FAVICON_RED = "favicon-red.svg"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -34,52 +36,6 @@ export async function copyToClipboard(content: string) {
} }
} }
const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
export const updateSystemList = (() => {
let isFetchingSystems = false
return async () => {
if (isFetchingSystems) {
return
}
isFetchingSystems = true
try {
const records = await pb
.collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) {
$systems.set(records)
} else {
verifyAuth()
}
} finally {
isFetchingSystems = false
}
}
})()
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$systems.set([])
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, { const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
@@ -110,47 +66,6 @@ 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",
@@ -193,32 +108,6 @@ export const chartTimeData: ChartTimeData = {
}, },
} }
/** Sets the correct width of the y axis in recharts based on the longest label */
export function useYAxisWidth() {
const [yAxisWidth, setYAxisWidth] = useState(0)
let maxChars = 0
let timeout: Timer
function updateYAxisWidth(str: string) {
if (str.length > maxChars) {
maxChars = str.length
const div = document.createElement("div")
div.className = "text-xs tabular-nums tracking-tighter table sr-only"
div.innerHTML = str
clearTimeout(timeout)
timeout = setTimeout(() => {
document.body.appendChild(div)
const width = div.offsetWidth + 24
if (width > yAxisWidth) {
setYAxisWidth(div.offsetWidth + 24)
}
document.body.removeChild(div)
})
}
return str
}
return { yAxisWidth, updateYAxisWidth }
}
/** Format number to x decimal places, without trailing zeros */ /** Format number to x decimal places, without trailing zeros */
export function toFixedFloat(num: number, digits: number) { export function toFixedFloat(num: number, digits: number) {
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits)) return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
@@ -241,20 +130,20 @@ export function decimalString(num: number, digits = 2) {
return formatter.format(num) return formatter.format(num)
} }
/** Get value from local storage */ /** Get value from local or session storage */
function getStorageValue(key: string, defaultValue: any) { function getStorageValue(key: string, defaultValue: any, storageInterface: Storage = localStorage) {
const saved = localStorage?.getItem(key) const saved = storageInterface?.getItem(key)
return saved ? JSON.parse(saved) : defaultValue return saved ? JSON.parse(saved) : defaultValue
} }
/** Hook to sync value in local storage */ /** Hook to sync value in local or session storage */
export function useLocalStorage<T>(key: string, defaultValue: T) { export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
key = `besz-${key}` key = `besz-${key}`
const [value, setValue] = useState(() => { const [value, setValue] = useState(() => {
return getStorageValue(key, defaultValue) return getStorageValue(key, defaultValue, storageInterface)
}) })
useEffect(() => { useEffect(() => {
localStorage?.setItem(key, JSON.stringify(value)) storageInterface?.setItem(key, JSON.stringify(value))
}, [key, value]) }, [key, value])
return [value, setValue] return [value, setValue]
@@ -329,24 +218,6 @@ export function formatBytes(
} }
} }
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
export const chartMargin = { top: 12 } export const chartMargin = { top: 12 }
/** /**
@@ -357,6 +228,21 @@ 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 = () => {
try { try {
@@ -436,15 +322,20 @@ export function debounce<T extends (...args: any[]) => any>(func: T, wait: numbe
} }
} }
/* returns the name of a system from its id */ // Cache for runOnce
export const getSystemNameFromId = (() => { const runOnceCache = new WeakMap<Function, { done: boolean; result: unknown }>()
const cache = new Map<string, string>() /** Run a function only once */
return (systemId: string): string => { export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
if (cache.has(systemId)) { return ((...args: Parameters<T>) => {
return cache.get(systemId)! let state = runOnceCache.get(fn)
if (!state) {
state = { done: false, result: undefined }
runOnceCache.set(fn, state)
} }
const sysName = $systems.get().find((s) => s.id === systemId)?.name ?? "" if (!state.done) {
cache.set(systemId, sysName) state.result = fn(...args)
return sysName state.done = true
} }
})() return state.result
}) as T
}

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-08-28 23:21\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"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# يوم} other {# أيام}}" msgstr "{0, plural, one {# يوم} other {# أيام}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ساعة} other {# ساعات}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# دقيقة} few {# دقائق} many {# دقيقة} other {# دقيقة}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "تم تحديد {0} من {1} صف"
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعة} other {# ساعات}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 ساعة" msgstr "1 ساعة"
@@ -125,6 +131,7 @@ msgstr "التنبيهات"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "جميع الأنظمة" msgstr "جميع الأنظمة"
@@ -252,6 +259,10 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "تحقق من خدمة الإشعارات الخاصة بك" msgstr "تحقق من خدمة الإشعارات الخاصة بك"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "انقر على نظام لعرض مزيد من المعلومات."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "انقر للنسخ" msgstr "انقر للنسخ"
@@ -426,6 +437,10 @@ msgstr "التوثيق"
msgid "Down" msgid "Down"
msgstr "معطل" msgstr "معطل"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "معطل ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "المدة" msgstr "المدة"
@@ -492,7 +507,7 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "فهرنهايت (°ف)" msgstr "فهرنهايت (°ف)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "فشل في المصادقة" msgstr "فشل في المصادقة"
@@ -753,6 +768,10 @@ msgstr "إيقاف مؤقت"
msgid "Paused" msgid "Paused"
msgstr "متوقف مؤقتا" msgstr "متوقف مؤقتا"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "متوقف مؤقتا ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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> لضمان تسليم التنبيهات."
@@ -774,7 +793,7 @@ msgstr "يرجى إنشاء حساب مسؤول"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع" msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "يرجى تسجيل الدخول مرة أخرى" msgstr "يرجى تسجيل الدخول مرة أخرى"
@@ -901,6 +920,7 @@ msgstr "الترتيب حسب"
msgid "State" msgid "State"
msgstr "الحالة" msgstr "الحالة"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "الحالة" msgstr "الحالة"
@@ -1055,6 +1075,7 @@ msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص
msgid "Unit preferences" msgid "Unit preferences"
msgstr "تفضيلات الوحدة" msgstr "تفضيلات الوحدة"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "رمز مميز عالمي" msgstr "رمز مميز عالمي"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "محدث في الوقت الحقيقي. انقر على نظام لعرض المعلومات." msgstr "قيد التشغيل ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

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-08-28 23:21\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"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# ден} other {# дни}}" msgstr "{0, plural, one {# ден} other {# дни}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# час} other {# часа}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# минута} few {# минути} many {# минути} other {# минути}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "{0} от {1} селектирани."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часа}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 час" msgstr "1 час"
@@ -125,6 +131,7 @@ msgstr "Тревоги"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Всички системи" msgstr "Всички системи"
@@ -252,6 +259,10 @@ msgstr "Провери log-овете за повече информация."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Провери услугата си за удостоверяване" msgstr "Провери услугата си за удостоверяване"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Кликнете върху система, за да видите повече информация."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Настисни за да копираш" msgstr "Настисни за да копираш"
@@ -426,6 +437,10 @@ msgstr "Документация"
msgid "Down" msgid "Down"
msgstr "Офлайн" msgstr "Офлайн"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Офлайн ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Продължителност" msgstr "Продължителност"
@@ -492,7 +507,7 @@ msgstr "Експортирай конфигурацията на системи
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Фаренхайт (°F)" msgstr "Фаренхайт (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Неуспешно удостоверяване" msgstr "Неуспешно удостоверяване"
@@ -753,6 +768,10 @@ msgstr "Пауза"
msgid "Paused" msgid "Paused"
msgstr "На пауза" msgstr "На пауза"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "На пауза ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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> за да се подсигуриш, че тревогите са доставени."
@@ -774,7 +793,7 @@ msgstr "Моля създай администраторски акаунт"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Моля активирай изскачащите прозорци за този сайт" msgstr "Моля активирай изскачащите прозорци за този сайт"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Моля влез отново" msgstr "Моля влез отново"
@@ -901,6 +920,7 @@ msgstr "Сортиране по"
msgid "State" msgid "State"
msgstr "Състояние" msgstr "Състояние"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Статус" msgstr "Статус"
@@ -1055,6 +1075,7 @@ msgstr "Задейства се, когато употребата на няко
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Предпочитания на единицата" msgstr "Предпочитания на единицата"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Универсален тоукън" msgstr "Универсален тоукън"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Актуализира се в реално време. Натисни на система за да видиш информация." msgstr "Нагоре ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

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-28 23:21\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"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# den} few {# dny} other {# dní}}" msgstr "{0, plural, one {# den} few {# dny} other {# dní}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} z {1} vybraných řádků." msgstr "{0} z {1} vybraných řádků."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 hodina" msgstr "1 hodina"
@@ -125,6 +131,7 @@ msgstr "Výstrahy"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Všechny systémy" msgstr "Všechny systémy"
@@ -252,6 +259,10 @@ msgstr "Pro více informací zkontrolujte logy."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Zkontrolujte službu upozornění" msgstr "Zkontrolujte službu upozornění"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klikněte na systém pro zobrazení více informací."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klikněte pro zkopírování" msgstr "Klikněte pro zkopírování"
@@ -426,6 +437,10 @@ msgstr "Dokumentace"
msgid "Down" msgid "Down"
msgstr "Nefunkční" msgstr "Nefunkční"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Nefunkční ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Doba trvání" msgstr "Doba trvání"
@@ -492,7 +507,7 @@ msgstr "Exportovat aktuální konfiguraci systémů."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheita (°F)" msgstr "Fahrenheita (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Ověření se nezdařilo" msgstr "Ověření se nezdařilo"
@@ -753,6 +768,10 @@ msgstr "Pozastavit"
msgid "Paused" msgid "Paused"
msgstr "Pozastaveno" msgstr "Pozastaveno"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pozastaveno ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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."
@@ -774,7 +793,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/utils.ts #: src/lib/api.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"
@@ -901,6 +920,7 @@ msgstr "Seřadit podle"
msgid "State" msgid "State"
msgstr "Stav" msgstr "Stav"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Stav" msgstr "Stav"
@@ -1055,6 +1075,7 @@ msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Předvolby jednotek" msgstr "Předvolby jednotek"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Univerzální token" msgstr "Univerzální token"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Funkční" msgstr "Funkční"
#: 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 "Up ({upSystemsLength})"
msgstr "Aktualizováno v reálném čase. Klepnutím na systém zobrazíte informace." msgstr "Funkční ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

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-08-28 23:21\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"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# day} other {# days}}" msgstr "{0, plural, one {# day} other {# days}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hour} other {# hours}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 time" msgstr "1 time"
@@ -125,6 +131,7 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Alle systemer" msgstr "Alle systemer"
@@ -252,6 +259,10 @@ msgstr "Tjek logfiler for flere detaljer."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Tjek din notifikationstjeneste" msgstr "Tjek din notifikationstjeneste"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klik for at kopiere" msgstr "Klik for at kopiere"
@@ -426,6 +437,10 @@ msgstr "Dokumentation"
msgid "Down" msgid "Down"
msgstr "Nede" msgstr "Nede"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Eksporter din nuværende systemkonfiguration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Kunne ikke godkende" msgstr "Kunne ikke godkende"
@@ -753,6 +768,10 @@ msgstr "Pause"
msgid "Paused" msgid "Paused"
msgstr "Sat på pause" msgstr "Sat på pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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."
@@ -774,7 +793,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/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Log venligst ind igen" msgstr "Log venligst ind igen"
@@ -901,6 +920,7 @@ msgstr "Sorter efter"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Udløser når brugen af en disk overstiger en tærskel"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Oppe" msgstr "Oppe"
#: 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 "Up ({upSystemsLength})"
msgstr "Opdateret i realtid. Klik på et system for at se information." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

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-08-28 23:21\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"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# Tag} other {# Tage}}" msgstr "{0, plural, one {# Tag} other {# Tage}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# Stunde} other {# Stunden}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# Minute} other {# Minuten}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} von {1} Zeile(n) ausgewählt." msgstr "{0} von {1} Zeile(n) ausgewählt."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Stunde} other {# Stunden}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 Stunde" msgstr "1 Stunde"
@@ -125,6 +131,7 @@ msgstr "Warnungen"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Alle Systeme" msgstr "Alle Systeme"
@@ -252,6 +259,10 @@ msgstr "Überprüfe die Protokolle für weitere Details."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Überprüfe deinen Benachrichtigungsdienst" msgstr "Überprüfe deinen Benachrichtigungsdienst"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klicke auf ein System, um weitere Informationen zu sehen."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Zum Kopieren klicken" msgstr "Zum Kopieren klicken"
@@ -426,6 +437,10 @@ msgstr "Dokumentation"
msgid "Down" msgid "Down"
msgstr "Offline" msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Dauer" msgstr "Dauer"
@@ -492,7 +507,7 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Authentifizierung fehlgeschlagen" msgstr "Authentifizierung fehlgeschlagen"
@@ -601,7 +616,7 @@ msgstr "Durchschnittliche Systemlast 5 Min"
#. Short label for load average #. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg" msgid "Load Avg"
msgstr "Durchschnittliche Last" msgstr "Systemlast"
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
@@ -753,6 +768,10 @@ msgstr "Pause"
msgid "Paused" msgid "Paused"
msgstr "Pausiert" msgstr "Pausiert"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausiert ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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."
@@ -774,7 +793,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/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Bitte melde dich erneut an" msgstr "Bitte melde dich erneut an"
@@ -901,6 +920,7 @@ msgstr "Sortieren nach"
msgid "State" msgid "State"
msgstr "Status" msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert übersc
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Einheiten" msgstr "Einheiten"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Universeller Token" msgstr "Universeller Token"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "aktiv" msgstr "aktiv"
#: 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 "Up ({upSystemsLength})"
msgstr "In Echtzeit aktualisiert. Klicke auf ein System, um Informationen anzuzeigen." msgstr "aktiv ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -18,16 +18,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# day} other {# days}}" msgstr "{0, plural, one {# day} other {# days}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hour} other {# hours}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} of {1} row(s) selected." msgstr "{0} of {1} row(s) selected."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 hour" msgstr "1 hour"
@@ -120,6 +126,7 @@ msgstr "Alerts"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "All Systems" msgstr "All Systems"
@@ -247,6 +254,10 @@ msgstr "Check logs for more details."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Check your notification service" msgstr "Check your notification service"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Click on a system to view more information."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Click to copy" msgstr "Click to copy"
@@ -421,6 +432,10 @@ msgstr "Documentation"
msgid "Down" msgid "Down"
msgstr "Down" msgstr "Down"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Down ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Duration" msgstr "Duration"
@@ -487,7 +502,7 @@ msgstr "Export your current systems configuration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Failed to authenticate" msgstr "Failed to authenticate"
@@ -748,6 +763,10 @@ msgstr "Pause"
msgid "Paused" msgid "Paused"
msgstr "Paused" msgstr "Paused"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Paused ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgstr "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
@@ -769,7 +788,7 @@ msgstr "Please create an admin account"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Please enable pop-ups for this site" msgstr "Please enable pop-ups for this site"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Please log in again" msgstr "Please log in again"
@@ -896,6 +915,7 @@ msgstr "Sort By"
msgid "State" msgid "State"
msgstr "State" msgstr "State"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1050,6 +1070,7 @@ msgstr "Triggers when usage of any disk exceeds a threshold"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Unit preferences" msgstr "Unit preferences"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Universal token" msgstr "Universal token"
@@ -1066,8 +1087,8 @@ msgid "Up"
msgstr "Up" msgstr "Up"
#: 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 "Up ({upSystemsLength})"
msgstr "Updated in real time. Click on a system to view information." msgstr "Up ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n" "Language: es\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# día} other {# días}}" msgstr "{0, plural, one {# día} other {# días}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hora} other {# horas}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuto} other {# minutos}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} de {1} fila(s) seleccionada(s)." msgstr "{0} de {1} fila(s) seleccionada(s)."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hora} other {# horas}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 hora" msgstr "1 hora"
@@ -40,7 +46,7 @@ msgstr "1 hora"
#. 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 "" msgstr "1 min"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 week" msgid "1 week"
@@ -53,7 +59,7 @@ msgstr "12 horas"
#. 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 "" msgstr "15 min"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "24 hours" msgid "24 hours"
@@ -66,7 +72,7 @@ msgstr "30 días"
#. 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 "" msgstr "5 min"
#. Table column #. Table column
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
@@ -125,6 +131,7 @@ msgstr "Alertas"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Todos los Sistemas" msgstr "Todos los Sistemas"
@@ -195,12 +202,12 @@ msgstr "Binario"
#: 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 "" msgstr "Bits (Kbps, Mbps, Gbps)"
#: 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 "" msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
@@ -217,7 +224,7 @@ msgstr "Precaución - posible pérdida de datos"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "" msgstr "Celsius (°C)"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change display units for metrics." msgid "Change display units for metrics."
@@ -252,6 +259,10 @@ msgstr "Revise los registros para más detalles."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Verifique su servicio de notificaciones" msgstr "Verifique su servicio de notificaciones"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Haga clic en un sistema para ver más información."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Haga clic para copiar" msgstr "Haga clic para copiar"
@@ -426,6 +437,10 @@ msgstr "Documentación"
msgid "Down" msgid "Down"
msgstr "Abajo" msgstr "Abajo"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Abajo ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Duración" msgstr "Duración"
@@ -490,9 +505,9 @@ msgstr "Exporte la configuración actual de sus sistemas."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Error al autenticar" msgstr "Error al autenticar"
@@ -753,6 +768,10 @@ msgstr "Pausar"
msgid "Paused" msgid "Paused"
msgstr "Pausado" msgstr "Pausado"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausado ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Por favor, <0>configure un servidor SMTP</0> para asegurar que las alertas sean entregadas." msgstr "Por favor, <0>configure un servidor SMTP</0> para asegurar que las alertas sean entregadas."
@@ -774,7 +793,7 @@ msgstr "Por favor, cree una cuenta de administrador"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite las ventanas emergentes para este sitio" msgstr "Por favor, habilite las ventanas emergentes para este sitio"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Por favor, inicie sesión de nuevo" msgstr "Por favor, inicie sesión de nuevo"
@@ -901,6 +920,7 @@ msgstr "Ordenar por"
msgid "State" msgid "State"
msgstr "Estado" msgstr "Estado"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Estado" msgstr "Estado"
@@ -998,7 +1018,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "" msgstr "Token"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1055,6 +1075,7 @@ msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Preferencias de unidad" msgstr "Preferencias de unidad"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Token universal" msgstr "Token universal"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Activo" msgstr "Activo"
#: 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 "Up ({upSystemsLength})"
msgstr "Actualizado en tiempo real. Haga clic en un sistema para ver la información." msgstr "Activo ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n" "Language: fa\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Persian\n" "Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# روز} other {# روز}}" msgstr "{0, plural, one {# روز} other {# روز}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ساعت} other {# ساعت}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# دقیقه} few {# دقیقه} many {# دقیقه} other {# دقیقه}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "{0} از {1} ردیف انتخاب شده است."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعت} other {# ساعت}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "۱ ساعت" msgstr "۱ ساعت"
@@ -125,6 +131,7 @@ msgstr "هشدارها"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "همه سیستم‌ها" msgstr "همه سیستم‌ها"
@@ -252,6 +259,10 @@ msgstr "برای جزئیات بیشتر، لاگ‌ها را بررسی کنی
msgid "Check your notification service" msgid "Check your notification service"
msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید" msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "برای مشاهده اطلاعات بیشتر روی یک سیستم کلیک کنید."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "برای کپی کردن کلیک کنید" msgstr "برای کپی کردن کلیک کنید"
@@ -426,6 +437,10 @@ msgstr "مستندات"
msgid "Down" msgid "Down"
msgstr "قطع" msgstr "قطع"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "قطع ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "مدت زمان" msgstr "مدت زمان"
@@ -492,7 +507,7 @@ msgstr "پیکربندی سیستم‌های فعلی خود را خارج کن
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "فارنهایت (°F)" msgstr "فارنهایت (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "احراز هویت ناموفق بود" msgstr "احراز هویت ناموفق بود"
@@ -753,6 +768,10 @@ msgstr "توقف"
msgid "Paused" msgid "Paused"
msgstr "مکث شده" msgstr "مکث شده"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "مکث شده ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>."
@@ -774,7 +793,7 @@ msgstr "لطفاً یک حساب مدیر ایجاد کنید"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "لطفاً پنجره‌های بازشو را برای این سایت فعال کنید" msgstr "لطفاً پنجره‌های بازشو را برای این سایت فعال کنید"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "لطفاً دوباره وارد شوید" msgstr "لطفاً دوباره وارد شوید"
@@ -901,6 +920,7 @@ msgstr "مرتب‌سازی بر اساس"
msgid "State" msgid "State"
msgstr "وضعیت" msgstr "وضعیت"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "وضعیت" msgstr "وضعیت"
@@ -1055,6 +1075,7 @@ msgstr "هنگامی که استفاده از هر دیسکی از یک آستا
msgid "Unit preferences" msgid "Unit preferences"
msgstr "تنظیمات واحدها" msgstr "تنظیمات واحدها"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "توکن جهانی" msgstr "توکن جهانی"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "به صورت لحظه‌ای به‌روزرسانی می‌شود. برای مشاهده اطلاعات، روی یک سیستم کلیک کنید." msgstr "فعال ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n" "Language: fr\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# jour} other {# jours}}" msgstr "{0, plural, one {# jour} other {# jours}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# heure} other {# heures}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# heure} other {# heures}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 heure" msgstr "1 heure"
@@ -125,6 +131,7 @@ msgstr "Alertes"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Tous les systèmes" msgstr "Tous les systèmes"
@@ -252,6 +259,10 @@ msgstr "Vérifiez les journaux pour plus de détails."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Vérifiez votre service de notification" msgstr "Vérifiez votre service de notification"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Cliquez pour copier" msgstr "Cliquez pour copier"
@@ -426,6 +437,10 @@ msgstr "Documentation"
msgid "Down" msgid "Down"
msgstr "Injoignable" msgstr "Injoignable"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Échec de l'authentification" msgstr "Échec de l'authentification"
@@ -753,6 +768,10 @@ msgstr "Pause"
msgid "Paused" msgid "Paused"
msgstr "En pause" msgstr "En pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Veuillez <0>configurer un serveur SMTP</0> pour garantir la livraison des alertes." msgstr "Veuillez <0>configurer un serveur SMTP</0> pour garantir la livraison des alertes."
@@ -774,7 +793,7 @@ msgstr "Veuillez créer un compte administrateur"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Veuillez activer les pop-ups pour ce site" msgstr "Veuillez activer les pop-ups pour ce site"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Veuillez vous reconnecter" msgstr "Veuillez vous reconnecter"
@@ -901,6 +920,7 @@ msgstr "Trier par"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Statut" msgstr "Statut"
@@ -1055,6 +1075,7 @@ msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Token universel" msgstr "Token universel"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Joignable" msgstr "Joignable"
#: 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 "Up ({upSystemsLength})"
msgstr "Mis à jour en temps réel. Cliquez sur un système pour voir les informations." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n" "Language: hr\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-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dan} other {# dani}}" msgstr "{0, plural, one {# dan} other {# dani}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# sat} other {# sati}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# sat} other {# sati}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 sat" msgstr "1 sat"
@@ -125,6 +131,7 @@ msgstr "Upozorenja"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Svi Sistemi" msgstr "Svi Sistemi"
@@ -252,6 +259,10 @@ msgstr "Provjerite logove za više detalja."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Provjerite Vaš servis notifikacija" msgstr "Provjerite Vaš servis notifikacija"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Pritisnite za kopiranje" msgstr "Pritisnite za kopiranje"
@@ -426,6 +437,10 @@ msgstr "Dokumentacija"
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Izvoz trenutne sistemske konfiguracije."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Provjera autentičnosti nije uspjela" msgstr "Provjera autentičnosti nije uspjela"
@@ -753,6 +768,10 @@ msgstr "Pauza"
msgid "Paused" msgid "Paused"
msgstr "Pauzirano" msgstr "Pauzirano"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku upozorenja." msgstr "Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku upozorenja."
@@ -774,7 +793,7 @@ msgstr "Molimo kreirajte administratorski račun"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Omogućite skočne prozore za ovu stranicu" msgstr "Omogućite skočne prozore za ovu stranicu"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Molimo prijavite se ponovno" msgstr "Molimo prijavite se ponovno"
@@ -901,6 +920,7 @@ msgstr "Sortiraj po"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Ažurirano odmah. Kliknite na sistem za više informacija." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n" "Language: hu\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Hungarian\n" "Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# nap} other {# nap}}" msgstr "{0, plural, one {# nap} other {# nap}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# óra} other {# óra}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# óra} other {# óra}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 óra" msgstr "1 óra"
@@ -125,6 +131,7 @@ msgstr "Riasztások"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Minden rendszer" msgstr "Minden rendszer"
@@ -252,6 +259,10 @@ msgstr "Ellenőrizd a naplót a további részletekért."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Ellenőrizd az értesítési szolgáltatásodat" msgstr "Ellenőrizd az értesítési szolgáltatásodat"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Kattints a másoláshoz" msgstr "Kattints a másoláshoz"
@@ -426,6 +437,10 @@ msgstr "Dokumentáció"
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Hitelesítés sikertelen" msgstr "Hitelesítés sikertelen"
@@ -753,6 +768,10 @@ msgstr "Szüneteltetés"
msgid "Paused" msgid "Paused"
msgstr "Szüneteltetve" msgstr "Szüneteltetve"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Kérjük, <0>konfigurálj egy SMTP szervert</0> az értesítések kézbesítésének biztosítása érdekében." msgstr "Kérjük, <0>konfigurálj egy SMTP szervert</0> az értesítések kézbesítésének biztosítása érdekében."
@@ -774,7 +793,7 @@ msgstr "Kérjük, hozzon létre egy admin fiókot"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Kérjük, engedélyezze a felugró ablakokat ezen az oldalon" msgstr "Kérjük, engedélyezze a felugró ablakokat ezen az oldalon"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Kérjük jelentkezz be újra" msgstr "Kérjük jelentkezz be újra"
@@ -901,6 +920,7 @@ msgstr "Rendezés"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Állapot" msgstr "Állapot"
@@ -1055,6 +1075,7 @@ msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Valós időben frissítve. Kattintson egy rendszerre az információk megtekintéséhez." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: is\n" "Language: is\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Icelandic\n" "Language-Team: Icelandic\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dagur} other {# dagar}}" msgstr "{0, plural, one {# dagur} other {# dagar}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# klukkustund} other {# klukkustundir}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# klukkustund} other {# klukkustundir}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 klukkustund" msgstr "1 klukkustund"
@@ -125,6 +131,7 @@ msgstr "Tilkynningar"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Öll kerfi" msgstr "Öll kerfi"
@@ -252,6 +259,10 @@ msgstr "Skoðaðu logga til að sjá meiri upplýsingar."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Athugaðu tilkynningaþjónustuna þína" msgstr "Athugaðu tilkynningaþjónustuna þína"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Smelltu til að afrita" msgstr "Smelltu til að afrita"
@@ -426,6 +437,10 @@ msgstr "Skjal"
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr ""
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Villa í auðkenningu" msgstr "Villa í auðkenningu"
@@ -753,6 +768,10 @@ msgstr "Pása"
msgid "Paused" msgid "Paused"
msgstr "Í bið" msgstr "Í bið"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "" msgstr ""
@@ -774,7 +793,7 @@ msgstr "Vinsamlegast búðu til admin aðgang"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Vinsamlegast skráðu þið inn aftur" msgstr "Vinsamlegast skráðu þið inn aftur"
@@ -901,6 +920,7 @@ msgstr "Raða eftir"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Staða" msgstr "Staða"
@@ -1055,6 +1075,7 @@ msgstr "Virkjast þegar diska notkun fer yfir þröskuld"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Uppfærist í rauntíma. Veldu kerfi til að skoða upplýsingar." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n" "Language: it\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-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# giorno} other {# giorni}}" msgstr "{0, plural, one {# giorno} other {# giorni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ora} other {# ore}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuto} other {# minuti}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} di {1} righe selezionate." msgstr "{0} di {1} righe selezionate."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ora} other {# ore}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 ora" msgstr "1 ora"
@@ -125,6 +131,7 @@ msgstr "Avvisi"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Tutti i Sistemi" msgstr "Tutti i Sistemi"
@@ -195,12 +202,12 @@ msgstr "Binario"
#: 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 "" msgstr "Bit (Kbps, Mbps, Gbps)"
#: 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 "" msgstr "Byte (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
@@ -217,11 +224,11 @@ msgstr "Attenzione - possibile perdita di dati"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "" msgstr "Celsius (°C)"
#: 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 "Modifica le unità di visualizzazione per le metriche."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change general application options." msgid "Change general application options."
@@ -252,6 +259,10 @@ msgstr "Controlla i log per maggiori dettagli."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Controlla il tuo servizio di notifica" msgstr "Controlla il tuo servizio di notifica"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Clicca su un sistema per visualizzare più informazioni."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Clicca per copiare" msgstr "Clicca per copiare"
@@ -272,7 +283,7 @@ msgstr "Conferma password"
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
msgid "Connection is down" msgid "Connection is down"
msgstr "" msgstr "La connessione è interrotta"
#: src/components/routes/settings/alerts-history-data-table.tsx #: 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
@@ -346,7 +357,7 @@ msgstr "Crea account"
#. Context: date created #. Context: date created
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Created" msgid "Created"
msgstr "" msgstr "Creato"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Critical (%)" msgid "Critical (%)"
@@ -390,7 +401,7 @@ msgstr "I/O Disco"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "" msgstr "Unità disco"
#: src/components/charts/disk-chart.tsx #: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -426,9 +437,13 @@ msgstr "Documentazione"
msgid "Down" msgid "Down"
msgstr "Offline" msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Durata"
#: 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
@@ -490,9 +505,9 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Autenticazione fallita" msgstr "Autenticazione fallita"
@@ -517,7 +532,7 @@ msgstr "Filtra..."
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint" msgid "Fingerprint"
msgstr "" msgstr "Impronta digitale"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -671,7 +686,7 @@ msgstr "Traffico di rete delle interfacce pubbliche"
#. 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 "Unità rete"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "No results found." msgid "No results found."
@@ -753,6 +768,10 @@ msgstr "Pausa"
msgid "Paused" msgid "Paused"
msgstr "In pausa" msgstr "In pausa"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "In pausa ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Si prega di <0>configurare un server SMTP</0> per garantire la consegna degli avvisi." msgstr "Si prega di <0>configurare un server SMTP</0> per garantire la consegna degli avvisi."
@@ -774,7 +793,7 @@ msgstr "Si prega di creare un account amministratore"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Si prega di abilitare i pop-up per questo sito" msgstr "Si prega di abilitare i pop-up per questo sito"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Si prega di accedere nuovamente" msgstr "Si prega di accedere nuovamente"
@@ -822,7 +841,7 @@ msgstr "Reimposta Password"
#: 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 "Risolto"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Resume" msgid "Resume"
@@ -834,7 +853,7 @@ msgstr "Ruota token"
#: 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 "Righe per pagina"
#: 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."
@@ -899,8 +918,9 @@ msgstr "Ordina per"
#. 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 "Stato"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Stato" msgstr "Stato"
@@ -922,7 +942,7 @@ msgstr "Sistema"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "" msgstr "Medie di carico del sistema nel tempo"
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Systems" msgid "Systems"
@@ -948,7 +968,7 @@ msgstr "Temperatura"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Temperature unit" msgid "Temperature unit"
msgstr "" msgstr "Unità temperatura"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
@@ -972,7 +992,7 @@ msgstr "Questa azione non può essere annullata. Questo eliminerà permanentemen
#: 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 "Questo eliminerà permanentemente tutti i record selezionati dal database."
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
@@ -1016,15 +1036,15 @@ msgstr "I token e le impronte digitali vengono utilizzati per autenticare le con
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "" msgstr "Si attiva quando la media di carico di 1 minuto supera una soglia"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "" msgstr "Si attiva quando la media di carico di 15 minuti supera una soglia"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "" msgstr "Si attiva quando la media di carico di 5 minuti supera una soglia"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
@@ -1053,8 +1073,9 @@ msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
#. 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 "Preferenze unità"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Token universale" msgstr "Token universale"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Attivo" msgstr "Attivo"
#: 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 "Up ({upSystemsLength})"
msgstr "Aggiornato in tempo reale. Clicca su un sistema per visualizzare le informazioni." msgstr "Attivo ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"
@@ -1101,7 +1122,7 @@ msgstr "Utenti"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Value" msgid "Value"
msgstr "" msgstr "Valore"
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "View" msgid "View"
@@ -1109,7 +1130,7 @@ msgstr "Vista"
#: 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 "" msgstr "Visualizza i tuoi 200 avvisi più recenti."
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Visible Fields" msgid "Visible Fields"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n" "Language: ja\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-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# 日} other {# 日}}" msgstr "{0, plural, one {# 日} other {# 日}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# 時間} other {# 時間}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# 分} few {# 分} many {# 分} other {# 分}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "{1}行のうち{0}行が選択されました。" msgstr "{1}行のうち{0}行が選択されました。"
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 時間} other {# 時間}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1時間" msgstr "1時間"
@@ -125,6 +131,7 @@ msgstr "アラート"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "すべてのシステム" msgstr "すべてのシステム"
@@ -252,6 +259,10 @@ msgstr "詳細についてはログを確認してください。"
msgid "Check your notification service" msgid "Check your notification service"
msgstr "通知サービスを確認してください" msgstr "通知サービスを確認してください"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "システムをクリックして詳細を表示します。"
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "クリックしてコピー" msgstr "クリックしてコピー"
@@ -426,6 +437,10 @@ msgstr "ドキュメント"
msgid "Down" msgid "Down"
msgstr "停止" msgstr "停止"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "停止 ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "期間" msgstr "期間"
@@ -492,7 +507,7 @@ msgstr "現在のシステム設定をエクスポートします。"
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)" msgstr "華氏 (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "認証に失敗しました" msgstr "認証に失敗しました"
@@ -753,6 +768,10 @@ msgstr "一時停止"
msgid "Paused" msgid "Paused"
msgstr "一時停止中" msgstr "一時停止中"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "一時停止 ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>してください。"
@@ -774,7 +793,7 @@ msgstr "管理者アカウントを作成してください"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "このサイトのポップアップを有効にしてください" msgstr "このサイトのポップアップを有効にしてください"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "再度ログインしてください" msgstr "再度ログインしてください"
@@ -901,6 +920,7 @@ msgstr "並び替え基準"
msgid "State" msgid "State"
msgstr "状態" msgstr "状態"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "ステータス" msgstr "ステータス"
@@ -1055,6 +1075,7 @@ msgstr "ディスクの使用量がしきい値を超えたときにトリガー
msgid "Unit preferences" msgid "Unit preferences"
msgstr "単位の設定" msgstr "単位の設定"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "ユニバーサルトークン" msgstr "ユニバーサルトークン"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "リアルタイムで更新されます。システムをクリックして情報を表示します。" msgstr "正常 ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n" "Language: ko\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-31 15:44\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Korean\n" "Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# 일} other {# 일}}" msgstr "{0, plural, one {# 일} other {# 일}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# 시간} other {# 시간}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# 분} few {# 분} many {# 분} other {# 분}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "{1}개의 행 중 {0}개가 선택되었습니다." msgstr "{1}개의 행 중 {0}개가 선택되었습니다."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 시간} other {# 시간}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1시간" msgstr "1시간"
@@ -125,6 +131,7 @@ msgstr "알림"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "모든 시스템" msgstr "모든 시스템"
@@ -252,6 +259,10 @@ msgstr "자세한 내용은 로그를 확인하세요."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "알림 서비스를 확인하세요." msgstr "알림 서비스를 확인하세요."
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "더 많은 정보를 보려면 시스템을 클릭하세요."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "클릭하여 복사" msgstr "클릭하여 복사"
@@ -426,6 +437,10 @@ msgstr "문서"
msgid "Down" msgid "Down"
msgstr "오프라인" msgstr "오프라인"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "오프라인 ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "기간" msgstr "기간"
@@ -492,7 +507,7 @@ msgstr "현재 시스템 구성 내보내기"
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "화씨 (°F)" msgstr "화씨 (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "인증 실패" msgstr "인증 실패"
@@ -753,6 +768,10 @@ msgstr "일시 중지"
msgid "Paused" msgid "Paused"
msgstr "일시 정지됨" msgstr "일시 정지됨"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "일시 정지됨 ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>하세요."
@@ -774,7 +793,7 @@ msgstr "관리자 계정을 생성하세요."
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "이 사이트에 대해 팝업을 활성화하세요." msgstr "이 사이트에 대해 팝업을 활성화하세요."
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "다시 로그인하세요." msgstr "다시 로그인하세요."
@@ -901,6 +920,7 @@ msgstr "정렬 기준"
msgid "State" msgid "State"
msgstr "상태" msgstr "상태"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "상태" msgstr "상태"
@@ -1055,6 +1075,7 @@ msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
msgid "Unit preferences" msgid "Unit preferences"
msgstr "단위 기본 설정" msgstr "단위 기본 설정"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "범용 토큰" msgstr "범용 토큰"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "실시간으로 업데이트됩니다. 시스템을 클릭하여 정보를 확인하세요." msgstr "온라인 ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n" "Language: nl\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-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dag} other {# dagen}}" msgstr "{0, plural, one {# dag} other {# dagen}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# uur} other {# uren}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuut} other {# minuten}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} van de {1} rij(en) geselecteerd." msgstr "{0} van de {1} rij(en) geselecteerd."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# uur} other {# uren}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 uur" msgstr "1 uur"
@@ -125,6 +131,7 @@ msgstr "Waarschuwingen"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Alle systemen" msgstr "Alle systemen"
@@ -252,6 +259,10 @@ msgstr "Controleer de logs voor meer details."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Controleer je meldingsservice" msgstr "Controleer je meldingsservice"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Klik op een systeem om meer informatie te bekijken."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klik om te kopiëren" msgstr "Klik om te kopiëren"
@@ -426,6 +437,10 @@ msgstr "Documentatie"
msgid "Down" msgid "Down"
msgstr "Offline" msgstr "Offline"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Offline ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Duur" msgstr "Duur"
@@ -492,7 +507,7 @@ msgstr "Exporteer je huidige systeemconfiguratie."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Authenticatie mislukt" msgstr "Authenticatie mislukt"
@@ -753,6 +768,10 @@ msgstr "Pauze"
msgid "Paused" msgid "Paused"
msgstr "Gepauzeerd" msgstr "Gepauzeerd"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Gepauzeerd ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>Configureer een SMTP-server </0> om ervoor te zorgen dat waarschuwingen worden afgeleverd." msgstr "<0>Configureer een SMTP-server </0> om ervoor te zorgen dat waarschuwingen worden afgeleverd."
@@ -774,7 +793,7 @@ msgstr "Maak een beheerdersaccount aan"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Activeer pop-ups voor deze website" msgstr "Activeer pop-ups voor deze website"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Meld je opnieuw aan" msgstr "Meld je opnieuw aan"
@@ -901,6 +920,7 @@ msgstr "Sorteren op"
msgid "State" msgid "State"
msgstr "Status" msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrij
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Eenheid voorkeuren" msgstr "Eenheid voorkeuren"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Universele token" msgstr "Universele token"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Online" msgstr "Online"
#: 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 "Up ({upSystemsLength})"
msgstr "In realtime bijgewerkt. Klik op een systeem om informatie te bekijken." msgstr "Online ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n" "Language: no\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Norwegian\n" "Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dag} other {# dager}}" msgstr "{0, plural, one {# dag} other {# dager}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# time} other {# timer}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# time} other {# timer}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 time" msgstr "1 time"
@@ -125,6 +131,7 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Alle Systemer" msgstr "Alle Systemer"
@@ -252,6 +259,10 @@ msgstr "Sjekk loggene for flere detaljer."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Sjekk din meldingstjeneste" msgstr "Sjekk din meldingstjeneste"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klikk for å kopiere" msgstr "Klikk for å kopiere"
@@ -426,6 +437,10 @@ msgstr "Dokumentasjon"
msgid "Down" msgid "Down"
msgstr "Nede" msgstr "Nede"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Eksporter din nåværende systemkonfigurasjon"
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Autentisering mislyktes" msgstr "Autentisering mislyktes"
@@ -753,6 +768,10 @@ msgstr "Pause"
msgid "Paused" msgid "Paused"
msgstr "Satt på Pause" msgstr "Satt på Pause"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Vennligst <0>konfigurer en SMTP-server</0> for å forsikre deg om at varsler blir levert." msgstr "Vennligst <0>konfigurer en SMTP-server</0> for å forsikre deg om at varsler blir levert."
@@ -774,7 +793,7 @@ msgstr "Vennligst opprett en admin-konto"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Vennligst aktiver pop-ups for nettsiden" msgstr "Vennligst aktiver pop-ups for nettsiden"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Vennligst logg inn på nytt" msgstr "Vennligst logg inn på nytt"
@@ -901,6 +920,7 @@ msgstr "Sorter Etter"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grensever
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Oppe" msgstr "Oppe"
#: 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 "Up ({upSystemsLength})"
msgstr "Oppdatert i sanntid. Klikk på et system for å se mer informasjon." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n" "Language: pl\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-13 15:36\n" "PO-Revision-Date: 2025-09-03 18:54\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}" msgstr "{0, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {godzinę} few {# godziny} many {# godzin} other {# godziny}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} z {1} wybranych wierszy." msgstr "{0} z {1} wybranych wierszy."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {godzinę} few {# godziny} many {# godzin} other {# godziny}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 godzina" msgstr "1 godzina"
@@ -125,6 +131,7 @@ msgstr "Alerty"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Wszystkie systemy" msgstr "Wszystkie systemy"
@@ -186,7 +193,7 @@ msgstr "Beszel obsługuje OpenID Connect i wielu dostawców uwierzytelniania OAu
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services." msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnych serwisami powiadomień." msgstr "Beszel używa <0>Shoutrrr</0> do integracji z popularnych serwisów powiadomień."
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Binary" msgid "Binary"
@@ -252,6 +259,10 @@ msgstr "Sprawdź logi, aby uzyskać więcej informacji."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Sprawdź swój serwis powiadomień" msgstr "Sprawdź swój serwis powiadomień"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Kliknij na system, aby zobaczyć więcej informacji."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Kliknij, aby skopiować" msgstr "Kliknij, aby skopiować"
@@ -426,6 +437,10 @@ msgstr "Dokumentacja"
msgid "Down" msgid "Down"
msgstr "Nie działa" msgstr "Nie działa"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Nie działa ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Czas trwania" msgstr "Czas trwania"
@@ -492,7 +507,7 @@ msgstr "Eksportuj aktualną konfigurację systemów."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Błąd autoryzacji" msgstr "Błąd autoryzacji"
@@ -753,6 +768,10 @@ msgstr "Pauza"
msgid "Paused" msgid "Paused"
msgstr "Wstrzymane" msgstr "Wstrzymane"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Wstrzymane ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Proszę <0>skonfigurować serwer SMTP</0>, aby zapewnić dostarczanie powiadomień." msgstr "Proszę <0>skonfigurować serwer SMTP</0>, aby zapewnić dostarczanie powiadomień."
@@ -774,7 +793,7 @@ msgstr "Utwórz konto administratora"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Włącz wyskakujące okna dla tej strony" msgstr "Włącz wyskakujące okna dla tej strony"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Zaloguj się ponownie" msgstr "Zaloguj się ponownie"
@@ -901,6 +920,7 @@ msgstr "Sortuj według"
msgid "State" msgid "State"
msgstr "Stan" msgstr "Stan"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony p
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Ustawienia jednostek" msgstr "Ustawienia jednostek"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Uniwersalny token" msgstr "Uniwersalny token"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Działa" msgstr "Działa"
#: 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 "Up ({upSystemsLength})"
msgstr "Aktualizowane w czasie rzeczywistym. Kliknij system, aby zobaczyć informacje." msgstr "Działa ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n" "Language: pt\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-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese\n" "Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,15 +23,21 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dia} other {# dias}}" msgstr "{0, plural, one {# dia} other {# dias}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# hora} other {# horas}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# minuto} other {# minutos}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr "{0} de {1} linha(s) selecionada(s)."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hora} other {# horas}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
@@ -125,6 +131,7 @@ msgstr "Alertas"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Todos os Sistemas" msgstr "Todos os Sistemas"
@@ -195,12 +202,12 @@ msgstr "Binário"
#: 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 "" msgstr "Bits (Kbps, Mbps, Gbps)"
#: 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 "" msgstr "Bytes (KB/s, MB/s, GB/s)"
#: src/components/charts/mem-chart.tsx #: src/components/charts/mem-chart.tsx
msgid "Cache / Buffers" msgid "Cache / Buffers"
@@ -217,11 +224,11 @@ msgstr "Cuidado - possível perda de dados"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Celsius (°C)" msgid "Celsius (°C)"
msgstr "" msgstr "Celsius (°C)"
#: 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 "Alterar unidades de exibição para métricas."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Change general application options." msgid "Change general application options."
@@ -252,6 +259,10 @@ msgstr "Verifique os logs para mais detalhes."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Verifique seu serviço de notificação" msgstr "Verifique seu serviço de notificação"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Clique em um sistema para ver mais informações."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Clique para copiar" msgstr "Clique para copiar"
@@ -272,7 +283,7 @@ msgstr "Confirmar senha"
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
msgid "Connection is down" msgid "Connection is down"
msgstr "" msgstr "A conexão está inativa"
#: src/components/routes/settings/alerts-history-data-table.tsx #: 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
@@ -390,7 +401,7 @@ msgstr "E/S de Disco"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Disk unit" msgid "Disk unit"
msgstr "" msgstr "Unidade de disco"
#: src/components/charts/disk-chart.tsx #: src/components/charts/disk-chart.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -426,6 +437,10 @@ msgstr "Documentação"
msgid "Down" msgid "Down"
msgstr "“Desligado”" msgstr "“Desligado”"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Inativo ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Duração" msgstr "Duração"
@@ -490,9 +505,9 @@ msgstr "Exporte a configuração atual dos seus sistemas."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Falha na autenticação" msgstr "Falha na autenticação"
@@ -517,7 +532,7 @@ msgstr "Filtrar..."
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Fingerprint" msgid "Fingerprint"
msgstr "" msgstr "Impressão digital"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -584,24 +599,24 @@ msgstr "Aspeto"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Load Average" msgid "Load Average"
msgstr "" msgstr "Carga Média"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Load Average 15m" msgid "Load Average 15m"
msgstr "" msgstr "Carga média 15m"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Load Average 1m" msgid "Load Average 1m"
msgstr "" msgstr "Carga média 1m"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Load Average 5m" msgid "Load Average 5m"
msgstr "" msgstr "Carga média 5m"
#. 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 "Carga Média"
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Log Out" msgid "Log Out"
@@ -671,7 +686,7 @@ msgstr "Tráfego de rede das interfaces públicas"
#. 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 "Unidade de rede"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "No results found." msgid "No results found."
@@ -722,7 +737,7 @@ msgstr "Página"
#. 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 "" msgstr "Página {0} de {1}"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
msgid "Pages / Settings" msgid "Pages / Settings"
@@ -753,6 +768,10 @@ msgstr "Pausar"
msgid "Paused" msgid "Paused"
msgstr "Pausado" msgstr "Pausado"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Pausado ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Por favor, <0>configure um servidor SMTP</0> para garantir que os alertas sejam entregues." msgstr "Por favor, <0>configure um servidor SMTP</0> para garantir que os alertas sejam entregues."
@@ -774,7 +793,7 @@ msgstr "Por favor, crie uma conta de administrador"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite pop-ups para este site" msgstr "Por favor, habilite pop-ups para este site"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Por favor, faça login novamente" msgstr "Por favor, faça login novamente"
@@ -822,7 +841,7 @@ msgstr "Redefinir Senha"
#: 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 "Resolvido"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Resume" msgid "Resume"
@@ -899,8 +918,9 @@ msgstr "Ordenar Por"
#. 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 "Estado"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -922,7 +942,7 @@ msgstr "Sistema"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "System load averages over time" msgid "System load averages over time"
msgstr "" msgstr "Médias de carga do sistema ao longo do tempo"
#: src/components/navbar.tsx #: src/components/navbar.tsx
msgid "Systems" msgid "Systems"
@@ -948,7 +968,7 @@ msgstr "Temperatura"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Temperature unit" msgid "Temperature unit"
msgstr "" msgstr "Unidade de temperatura"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Temperatures of system sensors" msgid "Temperatures of system sensors"
@@ -972,7 +992,7 @@ msgstr "Esta ação não pode ser desfeita. Isso excluirá permanentemente todos
#: 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 "Isso excluirá permanentemente todos os registros selecionados do banco de dados."
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Throughput of {extraFsName}" msgid "Throughput of {extraFsName}"
@@ -998,7 +1018,7 @@ msgstr "Alternar tema"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token" msgid "Token"
msgstr "" msgstr "Token"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1016,15 +1036,15 @@ msgstr "Tokens e impressões digitais são usados para autenticar conexões WebS
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold" msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr "" msgstr "Dispara quando a média de carga de 1 minuto excede um limite"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 15 minute load average exceeds a threshold" msgid "Triggers when 15 minute load average exceeds a threshold"
msgstr "" msgstr "Dispara quando a média de carga de 15 minutos excede um limite"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when 5 minute load average exceeds a threshold" msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "" msgstr "Dispara quando a média de carga de 5 minutos excede um limite"
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold" msgid "Triggers when any sensor exceeds a threshold"
@@ -1053,8 +1073,9 @@ msgstr "Dispara quando o uso de qualquer disco excede um limite"
#. 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 "Preferências de unidade"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Token universal" msgstr "Token universal"
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "“Ligado”" msgstr "“Ligado”"
#: 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 "Up ({upSystemsLength})"
msgstr "Atualizado em tempo real. Clique em um sistema para ver informações." msgstr "Ativo ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n" "Language: ru\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 02:49\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# день} other {# дней}}" msgstr "{0, plural, one {# день} other {# дней}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# час} other {# часов}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "Выбрано {0} из {1} строк."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часов}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 час" msgstr "1 час"
@@ -125,6 +131,7 @@ msgstr "Оповещения"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Все системы" msgstr "Все системы"
@@ -252,6 +259,10 @@ msgstr "Проверьте журналы для получения более
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Проверьте ваш сервис уведомлений" msgstr "Проверьте ваш сервис уведомлений"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Нажмите, чтобы скопировать" msgstr "Нажмите, чтобы скопировать"
@@ -426,6 +437,10 @@ msgstr "Документация"
msgid "Down" msgid "Down"
msgstr "Не в сети" msgstr "Не в сети"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Длительность" msgstr "Длительность"
@@ -492,7 +507,7 @@ msgstr "Экспортируйте текущую конфигурацию си
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Фаренгейт (°F)" msgstr "Фаренгейт (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Не удалось аутентифицировать" msgstr "Не удалось аутентифицировать"
@@ -753,6 +768,10 @@ msgstr "Пауза"
msgid "Paused" msgid "Paused"
msgstr "Пауза" msgstr "Пауза"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>, чтобы гарантировать доставку оповещений."
@@ -774,7 +793,7 @@ msgstr "Пожалуйста, создайте учетную запись ад
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Пожалуйста, включите всплывающие окна для этого сайта" msgstr "Пожалуйста, включите всплывающие окна для этого сайта"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Пожалуйста, войдите снова" msgstr "Пожалуйста, войдите снова"
@@ -901,6 +920,7 @@ msgstr "Сортировать по"
msgid "State" msgid "State"
msgstr "Состояние" msgstr "Состояние"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Статус" msgstr "Статус"
@@ -1055,6 +1075,7 @@ msgstr "Срабатывает, когда использование любог
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Параметры единиц измерения" msgstr "Параметры единиц измерения"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Универсальный токен" msgstr "Универсальный токен"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Обновляется в реальном времени. Нажмите на систему, чтобы просмотреть информацию." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n" "Language: sl\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Slovenian\n" "Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n" "Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dan} two {# dneva} few {# dni} other {# dni}}" msgstr "{0, plural, one {# dan} two {# dneva} few {# dni} other {# dni}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# ura} two {# uri} few {# ur} other {# ur}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "" msgstr ""
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ura} two {# uri} few {# ur} other {# ur}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 ura" msgstr "1 ura"
@@ -125,6 +131,7 @@ msgstr "Opozorila"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Vsi sistemi" msgstr "Vsi sistemi"
@@ -252,6 +259,10 @@ msgstr "Za več podrobnosti preverite dnevnike."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Preverite storitev obveščanja" msgstr "Preverite storitev obveščanja"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klikni za kopiranje" msgstr "Klikni za kopiranje"
@@ -426,6 +437,10 @@ msgstr "Dokumentacija"
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Izvozi trenutne nastavitve sistema."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Preverjanje pristnosti ni uspelo" msgstr "Preverjanje pristnosti ni uspelo"
@@ -753,6 +768,10 @@ msgstr "Premor"
msgid "Paused" msgid "Paused"
msgstr "Zaustavljeno" msgstr "Zaustavljeno"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>Nastavite strežnik SMTP</0>, da zagotovite dostavo opozoril." msgstr "<0>Nastavite strežnik SMTP</0>, da zagotovite dostavo opozoril."
@@ -774,7 +793,7 @@ msgstr "Ustvarite skrbniški račun"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Omogočite pojavna okna za to spletno mesto" msgstr "Omogočite pojavna okna za to spletno mesto"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Prosimo, prijavite se znova" msgstr "Prosimo, prijavite se znova"
@@ -901,6 +920,7 @@ msgstr "Razvrsti po"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Sproži se, ko uporaba katerega koli diska preseže prag"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Posodobljeno v realnem času. Za ogled informacij kliknite na sistem." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n" "Language: sv\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-01 23:21\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Swedish\n" "Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# dag} other {# dagar}}" msgstr "{0, plural, one {# dag} other {# dagar}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# timme} other {# timmar}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr ""
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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} av {1} rad(er) valda." msgstr "{0} av {1} rad(er) valda."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# timme} other {# timmar}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 timme" msgstr "1 timme"
@@ -125,6 +131,7 @@ msgstr "Larm"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Alla system" msgstr "Alla system"
@@ -252,6 +259,10 @@ msgstr "Kontrollera loggarna för mer information."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Kontrollera din aviseringstjänst" msgstr "Kontrollera din aviseringstjänst"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr ""
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Klicka för att kopiera" msgstr "Klicka för att kopiera"
@@ -426,6 +437,10 @@ msgstr "Dokumentation"
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
@@ -492,7 +507,7 @@ msgstr "Exportera din nuvarande systemkonfiguration."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "" msgstr ""
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Autentisering misslyckades" msgstr "Autentisering misslyckades"
@@ -753,6 +768,10 @@ msgstr "Paus"
msgid "Paused" msgid "Paused"
msgstr "Pausad" msgstr "Pausad"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr ""
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Vänligen <0>konfigurera en SMTP-server</0> för att säkerställa att larm levereras." msgstr "Vänligen <0>konfigurera en SMTP-server</0> för att säkerställa att larm levereras."
@@ -774,7 +793,7 @@ msgstr "Vänligen skapa ett administratörskonto"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Vänligen aktivera popup-fönster för den här webbplatsen" msgstr "Vänligen aktivera popup-fönster för den här webbplatsen"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Vänligen logga in igen" msgstr "Vänligen logga in igen"
@@ -901,6 +920,7 @@ msgstr "Sortera efter"
msgid "State" msgid "State"
msgstr "" msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -1055,6 +1075,7 @@ msgstr "Utlöses när användningen av någon disk överskrider ett tröskelvär
msgid "Unit preferences" msgid "Unit preferences"
msgstr "" msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "" msgstr ""
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Uppdateras i realtid. Klicka på ett system för att visa information." msgstr ""
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n" "Language: tr\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Turkish\n" "Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# gün} other {# gün}}" msgstr "{0, plural, one {# gün} other {# gün}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# saat} other {# saat}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# dakika} few {# dakika} many {# dakika} other {# dakika}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "{1} satırdan {0} tanesi seçildi." msgstr "{1} satırdan {0} tanesi seçildi."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# saat} other {# saat}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 saat" msgstr "1 saat"
@@ -125,6 +131,7 @@ msgstr "Uyarılar"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Tüm Sistemler" msgstr "Tüm Sistemler"
@@ -178,7 +185,7 @@ msgstr "Bant Genişliği"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Battery" msgid "Battery"
msgstr "" msgstr "Pil"
#: 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."
@@ -229,12 +236,12 @@ msgstr "Genel uygulama seçeneklerini değiştirin."
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Charge" msgid "Charge"
msgstr "" msgstr "Şarj"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Charging" msgid "Charging"
msgstr "" msgstr "Şarj oluyor"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Chart options" msgid "Chart options"
@@ -252,6 +259,10 @@ msgstr "Daha fazla ayrıntı için günlükleri kontrol edin."
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Bildirim hizmetinizi kontrol edin" msgstr "Bildirim hizmetinizi kontrol edin"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Daha fazla bilgi görmek için bir sisteme tıklayın."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Kopyalamak için tıklayın" msgstr "Kopyalamak için tıklayın"
@@ -355,7 +366,7 @@ msgstr "Kritik (%)"
#. Context: Battery state #. Context: Battery state
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Current state" msgid "Current state"
msgstr "" msgstr "Mevcut durum"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/home.tsx #: src/components/routes/home.tsx
@@ -378,7 +389,7 @@ msgstr "Parmak izini sil"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Discharging" msgid "Discharging"
msgstr "" msgstr "Boşalıyor"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Disk" msgid "Disk"
@@ -426,6 +437,10 @@ msgstr "Dokümantasyon"
msgid "Down" msgid "Down"
msgstr "Kapalı" msgstr "Kapalı"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Kapalı ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Süre" msgstr "Süre"
@@ -447,7 +462,7 @@ msgstr "E-posta bildirimleri"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Empty" msgid "Empty"
msgstr "" msgstr "Boş"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Enter email address to reset password" msgid "Enter email address to reset password"
@@ -492,7 +507,7 @@ msgstr "Mevcut sistem yapılandırmanızı dışa aktarın."
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Fahrenhayt (°F)" msgstr "Fahrenhayt (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Kimlik doğrulama başarısız" msgstr "Kimlik doğrulama başarısız"
@@ -530,7 +545,7 @@ msgstr "Şifrenizi mi unuttunuz?"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Full" msgid "Full"
msgstr "" msgstr "Dolu"
#. Context: General settings #. Context: General settings
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
@@ -559,7 +574,7 @@ msgstr "Host / IP"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Idle" msgid "Idle"
msgstr "" msgstr "Boşta"
#: 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."
@@ -753,6 +768,10 @@ msgstr "Duraklat"
msgid "Paused" msgid "Paused"
msgstr "Duraklatıldı" msgstr "Duraklatıldı"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Duraklatıldı ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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 "Uyarıların teslim edilmesini sağlamak için lütfen bir SMTP sunucusu <0>yapılandırın</0>." msgstr "Uyarıların teslim edilmesini sağlamak için lütfen bir SMTP sunucusu <0>yapılandırın</0>."
@@ -774,7 +793,7 @@ msgstr "Lütfen bir yönetici hesabı oluşturun"
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Lütfen bu site için açılır pencereleri etkinleştirin" msgstr "Lütfen bu site için açılır pencereleri etkinleştirin"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Lütfen tekrar giriş yapın" msgstr "Lütfen tekrar giriş yapın"
@@ -901,6 +920,7 @@ msgstr "Sıralama Ölçütü"
msgid "State" msgid "State"
msgstr "Durum" msgstr "Durum"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Durum" msgstr "Durum"
@@ -998,7 +1018,7 @@ msgstr "Temayı değiştir"
#: 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 "Token"
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
@@ -1055,6 +1075,7 @@ msgstr "Herhangi bir diskin kullanımı bir eşiği aştığında tetiklenir"
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Birim tercihleri" msgstr "Birim tercihleri"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Evrensel token" msgstr "Evrensel token"
@@ -1062,7 +1083,7 @@ msgstr "Evrensel token"
#. Context: Battery state #. Context: Battery state
#: src/lib/i18n.ts #: src/lib/i18n.ts
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr "Bilinmiyor"
#. Context: System is up #. Context: System is up
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -1071,8 +1092,8 @@ msgid "Up"
msgstr "Açık" msgstr "Açık"
#: 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 "Up ({upSystemsLength})"
msgstr "Gerçek zamanlı olarak güncellenir. Bilgileri görüntülemek için bir sisteme tıklayın." msgstr "ık ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n" "Language: uk\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-07-25 22:44\n" "PO-Revision-Date: 2025-08-30 16:20\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Ukrainian\n" "Language-Team: Ukrainian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n" "Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -23,16 +23,22 @@ msgstr ""
msgid "{0, plural, one {# day} other {# days}}" msgid "{0, plural, one {# day} other {# days}}"
msgstr "{0, plural, one {# день} few {# дні} many {# днів} other {# дня}}" msgstr "{0, plural, one {# день} few {# дні} many {# днів} other {# дня}}"
#. placeholder {0}: Math.trunc(system.info.u / 3600)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr "{0, plural, one {# година} few {# години} many {# годин} other {# години}}"
#. placeholder {0}: Math.trunc(system.info.u / 60)
#: src/components/routes/system.tsx
msgid "{0, plural, one {# minute} few {# minutes} many {# minutes} other {# minutes}}"
msgstr "{0, plural, one {# хвилина} few {# хвилини} many {# хвилин} other {# хвилини}}"
#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length #. placeholder {0}: table.getFilteredSelectedRowModel().rows.length
#. 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 "Вибрано {0} з {1} рядків."
#: src/components/routes/system.tsx
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# година} few {# години} many {# годин} other {# години}}"
#: src/lib/utils.ts #: src/lib/utils.ts
msgid "1 hour" msgid "1 hour"
msgstr "1 година" msgstr "1 година"
@@ -125,6 +131,7 @@ msgstr "Сповіщення"
#: src/components/alerts/alerts-sheet.tsx #: 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
msgid "All Systems" msgid "All Systems"
msgstr "Всі системи" msgstr "Всі системи"
@@ -252,6 +259,10 @@ msgstr "Перевірте журнали для отримання додатк
msgid "Check your notification service" msgid "Check your notification service"
msgstr "Перевірте свій сервіс сповіщень" msgstr "Перевірте свій сервіс сповіщень"
#: src/components/systems-table/systems-table.tsx
msgid "Click on a system to view more information."
msgstr "Натисніть на систему, щоб переглянути більше інформації."
#: src/components/ui/input-copy.tsx #: src/components/ui/input-copy.tsx
msgid "Click to copy" msgid "Click to copy"
msgstr "Натисніть, щоб скопіювати" msgstr "Натисніть, щоб скопіювати"
@@ -426,6 +437,10 @@ msgstr "Документація"
msgid "Down" msgid "Down"
msgstr "Не працює" msgstr "Не працює"
#: src/components/systems-table/systems-table.tsx
msgid "Down ({downSystemsLength})"
msgstr "Не працює ({downSystemsLength})"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
msgid "Duration" msgid "Duration"
msgstr "Тривалість" msgstr "Тривалість"
@@ -492,7 +507,7 @@ msgstr "Експортуйте поточну конфігурацію сист
msgid "Fahrenheit (°F)" msgid "Fahrenheit (°F)"
msgstr "Фаренгейт (°F)" msgstr "Фаренгейт (°F)"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Failed to authenticate" msgid "Failed to authenticate"
msgstr "Не вдалося автентифікувати" msgstr "Не вдалося автентифікувати"
@@ -753,6 +768,10 @@ msgstr "Призупинити"
msgid "Paused" msgid "Paused"
msgstr "Призупинено" msgstr "Призупинено"
#: src/components/systems-table/systems-table.tsx
msgid "Paused ({pausedSystemsLength})"
msgstr "Призупинено ({pausedSystemsLength})"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
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>, щоб забезпечити доставку сповіщень."
@@ -774,7 +793,7 @@ msgstr "Будь ласка, створіть адміністративний
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Будь ласка, увімкніть спливаючі вікна для цього сайту" msgstr "Будь ласка, увімкніть спливаючі вікна для цього сайту"
#: src/lib/utils.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Будь ласка, увійдіть знову" msgstr "Будь ласка, увійдіть знову"
@@ -901,6 +920,7 @@ msgstr "Сортувати за"
msgid "State" msgid "State"
msgstr "Стан" msgstr "Стан"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Status" msgid "Status"
msgstr "Статус" msgstr "Статус"
@@ -1055,6 +1075,7 @@ msgstr "Спрацьовує, коли використання будь-яко
msgid "Unit preferences" msgid "Unit preferences"
msgstr "Налаштування одиниць вимірювання" msgstr "Налаштування одиниць вимірювання"
#: src/components/command-palette.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Universal token" msgid "Universal token"
msgstr "Універсальний токен" msgstr "Універсальний токен"
@@ -1071,8 +1092,8 @@ 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 "Up ({upSystemsLength})"
msgstr "Оновлюється в реальному часі. Натисніть на систему, щоб переглянути інформацію." msgstr "Працює ({upSystemsLength})"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Uptime" msgid "Uptime"

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