Compare commits

...

61 Commits

Author SHA1 Message Date
henrygd
2bd85e04fc add experimental nvml gpu collector (#1522) 2025-12-21 17:10:42 -05:00
henrygd
f6ab5f2af1 refactor: rm diskinfo abstraction from smart-table.tsx 2025-12-21 12:25:12 -05:00
Sven van Ginkel
7d943633a3 fix capacity sorting in smart table (#1551) 2025-12-21 12:21:44 -05:00
henrygd
7fff3c999a update changelog and comments 2025-12-19 16:27:39 -05:00
henrygd
a9068a11a9 add SMART_INTERVAL env var with background smart data fetching 2025-12-19 16:14:31 -05:00
henrygd
d3d102516c refactor system.tsx: change null fallback for details 2025-12-19 00:45:15 -05:00
henrygd
32131439f9 fix systemd table visibility after moving os info to system_details
- also update bun.lock
2025-12-19 00:37:46 -05:00
Jesper Ek
d17685c540 fix non unique fingerprint (#1556) 2025-12-18 18:21:19 -05:00
henrygd
e59f8eee36 add system_details collection for infrequently updated data
- add includedetails flag to data requests for better efficiency
- integrate docker host info api for better os detection
- pull more OS details as well as cpu arch
- separate info bar component and refactor for new info
2025-12-18 17:26:05 -05:00
henrygd
35329abcbd agent install: improve freebsd checksum verification (#1526) 2025-12-12 15:32:55 -05:00
henrygd
ee7741c3ab add --url and --token command line args for agent (#1524) 2025-12-12 13:58:44 -05:00
David Davis
ab0803b2da Upgrade react to address CVE-2025-55182
More info: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
2025-12-12 07:42:29 -05:00
henrygd
96196a353c smart: fallback to nvme namespace path if base controller path fails (#1504) 2025-12-10 14:09:20 -05:00
henrygd
2a8796c38d update hub install script to support freebsd 2025-12-09 15:08:59 -05:00
Zero2A11
c8d4f7427d fix: When there is no client, LoaderCircle will always transfer (#1511) 2025-12-09 11:01:13 -05:00
henrygd
8d41a797d3 add battery charge to systems table 2025-12-08 18:20:51 -05:00
henrygd
570e1cbf40 pin alpine version to 3.22 2025-12-08 18:14:14 -05:00
henrygd
4c9b00a066 add low battery alert (#1507) 2025-12-08 15:19:46 -05:00
henrygd
7d1f8bb180 raise smartctl timeout to 15 seconds (#1465) 2025-12-05 14:59:11 -05:00
henrygd
3a6caeb06e add serbian 2025-12-05 14:21:50 -05:00
henrygd
9e67245e60 bump minimum go version to 1.25.5 2025-12-02 19:04:40 -05:00
henrygd
b7a95d5d76 release 0.17.0 2025-12-02 18:52:12 -05:00
henrygd
fe550c5901 refactor decodeDockerLogStream 2025-12-02 18:22:22 -05:00
henrygd
8aac0a571a update translations 2025-12-02 17:58:55 -05:00
henrygd
c506b8b0ad pull translations 2025-12-02 16:57:12 -05:00
henrygd
a6e84c207e update changelog 2025-11-30 16:24:05 -05:00
henrygd
249402eaed add hub builds for windows and freebsd 2025-11-30 15:25:31 -05:00
henrygd
394c476f2a strip ansi escape sequences from docker logs (#1478) 2025-11-30 14:36:00 -05:00
henrygd
86e8a141ea add DISK_USAGE_CACHE env var (#1426) 2025-11-30 14:21:00 -05:00
henrygd
53a7e06dcf upgrade go dependencies 2025-11-30 13:55:25 -05:00
henrygd
11edabd09f add alerts for smart failures 2025-11-30 13:52:31 -05:00
henrygd
41a3d9359f add dedicated S.M.A.R.T. page with persistent device storage
- Add /smart route to view SMART data across all systems
- Store SMART devices in new smart_devices collection
- Auto-fetch SMART data when system first comes online
- Add refresh/delete actions per device with realtime updates
- Add navbar and command palette entries
2025-11-30 13:32:37 -05:00
henrygd
5dfc5f247f update extra disk indicators in systems table to display max 3 states 2025-11-30 12:32:16 -05:00
henrygd
9804c8a31a update translation strings 2025-11-29 20:29:07 -05:00
Pavel Pikta
4d05bfdff0 feat: add crossorigin attribute to manifest link (#1457)
Signed-off-by: Pavel Pikta <pavel_pikta@epam.com>
2025-11-26 19:41:54 -05:00
henrygd
0388401a9e change layout of extra disks in all systems table (#1409) 2025-11-25 16:23:48 -05:00
henrygd
162c548010 quiet hours refactoring: change 'future' to 'inactive' 2025-11-24 19:12:35 -05:00
henrygd
888b4a57e5 add quiet hours to silence alerts during specific time periods (#265) 2025-11-24 17:35:28 -05:00
henrygd
26d367b188 add clear button to filter inputs in all systems and containers tables (#1447) 2025-11-19 17:58:58 -05:00
henrygd
ca4988951f add SKIP_SYSTEMD env var (#1448) 2025-11-19 17:21:30 -05:00
zjkal
c7a50dd74d fix: Fix issue where the Add System button is visible to read-only users (#1442)
移除按钮的hidden类并提前检查只读用户状态返回null
2025-11-19 16:38:37 -05:00
Frederik Ring
00fbf5c9c3 Font ligatures create unwanted artifacts in random ids (#1434) 2025-11-19 16:36:48 -05:00
henrygd
4bfe9dd5ad add missing systemd methods for nonlinux 2025-11-14 17:28:40 -05:00
henrygd
e159a75b79 update language files 2025-11-14 17:24:51 -05:00
henrygd
a69686125e release 0.16.1 2025-11-14 16:39:24 -05:00
henrygd
3eb025ded2 make sure distroless image gets :latest tag in workflow 2025-11-14 16:21:17 -05:00
henrygd
1d0e646094 update changelog and go deps 2025-11-14 16:05:36 -05:00
henrygd
32c8e047e3 update cpu / container axis datamax calculations 2025-11-14 15:45:18 -05:00
henrygd
3650482b09 refactor: move getRootMountPoint to disk.go 2025-11-14 14:06:46 -05:00
Arush Wadhawan
79adfd2c0d fix: detect and handle immutable filesystems like Fedora Silverblue (#1405) 2025-11-14 14:03:26 -05:00
Sven van Ginkel
779dcc62aa chore: update actions to lock issues and skip PRs (#1419) 2025-11-14 13:58:13 -05:00
henrygd
abe39c1a0a update bun.lockb to text-based bun.lock 2025-11-14 13:54:40 -05:00
henrygd
bd41ad813c change alert history pagination to use local storage instead of saving to user settings 2025-11-14 13:54:23 -05:00
Arush Wadhawan
77fe63fb63 feat: add alert history page size preference with persistence (#1404) 2025-11-14 13:37:46 -05:00
henrygd
f61ba202d8 remove matrix from list of notification services that support title param (#1406) 2025-11-14 13:27:23 -05:00
henrygd
e1067fa1a3 make layout width adjustable 2025-11-13 18:50:47 -05:00
henrygd
0a3eb898ae truncate device name in smart table (#1416) 2025-11-13 16:41:15 -05:00
evrial
6c33e9dc93 Set a dynamic upper domain on the YAxis for container chart (#1412) 2025-11-13 16:28:37 -05:00
henrygd
f8ed6ce705 refactor: fix nan when net value is undefined in systems table 2025-11-13 16:25:21 -05:00
henrygd
f64478b75e add SERVICE_PATTERNS env var (#1153) 2025-11-13 16:11:24 -05:00
henrygd
854a3697d7 add services column to all systems table 2025-11-13 15:09:48 -05:00
119 changed files with 10735 additions and 4633 deletions

View File

@@ -10,6 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
include:
# henrygd/beszel
@@ -24,20 +25,18 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# henrygd/beszel-agent
# henrygd/beszel-agent:alpine
- image: henrygd/beszel-agent
dockerfile: ./internal/dockerfile_agent
dockerfile: ./internal/dockerfile_agent_alpine
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
type=raw,value=alpine
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
# henrygd/beszel-agent-nvidia
- image: henrygd/beszel-agent-nvidia
@@ -67,18 +66,6 @@ jobs:
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# henrygd/beszel-agent:alpine
- image: henrygd/beszel-agent
dockerfile: ./internal/dockerfile_agent_alpine
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=alpine
type=semver,pattern={{version}}-alpine
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
# ghcr.io/henrygd/beszel
- image: ghcr.io/${{ github.repository }}/beszel
dockerfile: ./internal/dockerfile_hub
@@ -146,6 +133,19 @@ jobs:
type=semver,pattern={{major}}.{{minor}}-alpine
type=semver,pattern={{major}}-alpine
# henrygd/beszel-agent (keep at bottom so it gets built after :alpine and gets the latest tag)
- image: henrygd/beszel-agent
dockerfile: ./internal/dockerfile_agent
registry: docker.io
username_secret: DOCKERHUB_USERNAME
password_secret: DOCKERHUB_TOKEN
tags: |
type=raw,value=edge
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
permissions:
contents: read
packages: write

View File

@@ -10,12 +10,25 @@ permissions:
pull-requests: write
jobs:
lock-inactive:
name: Lock Inactive Issues
runs-on: ubuntu-24.04
steps:
- uses: klaasnicolaas/action-inactivity-lock@v1.1.3
id: lock
with:
days-inactive-issues: 14
lock-reason-issues: ""
# Action can not skip PRs, set it to 100 years to cover it.
days-inactive-prs: 36524
lock-reason-prs: ""
close-stale:
name: Close Stale Issues
runs-on: ubuntu-24.04
steps:
- name: Close Stale Issues
uses: actions/stale@v9
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -32,6 +45,8 @@ jobs:
# Timing
days-before-issue-stale: 14
days-before-issue-close: 7
# Action can not skip PRs, set it to 100 years to cover it.
days-before-pr-stale: 36524
# Labels
stale-issue-label: 'stale'

View File

@@ -16,10 +16,21 @@ builds:
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
- arm
ignore:
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: arm
- id: beszel-agent
binary: beszel-agent
@@ -86,6 +97,9 @@ archives:
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
format_overrides:
- goos: windows
formats: [zip]
nfpms:
- id: beszel-agent

View File

@@ -12,10 +12,12 @@ import (
"path/filepath"
"strings"
"sync"
"time"
"github.com/gliderlabs/ssh"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/host"
gossh "golang.org/x/crypto/ssh"
@@ -29,12 +31,15 @@ type Agent struct {
fsNames []string // List of filesystem device names being monitored
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
diskPrev map[uint16]map[string]prevDisk // Previous disk I/O counters per cache interval
diskUsageCacheDuration time.Duration // How long to cache disk usage (to avoid waking sleeping disks)
lastDiskUsageUpdate time.Time // Last time disk usage was collected
netInterfaces map[string]struct{} // Stores all valid network interfaces
netIoStats map[uint16]system.NetIoStats // Keeps track of bandwidth usage per cache interval
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
dockerManager *dockerManager // Manages Docker API requests
sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info
systemInfo system.Info // Host system info (dynamic)
systemDetails system.Details // Host system details (static, once-per-connection)
gpuManager *GPUManager // Manages GPU data
cache *systemDataCache // Cache for system stats based on cache time
connectionManager *ConnectionManager // Channel to signal connection events
@@ -69,6 +74,17 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
agent.memCalc, _ = GetEnv("MEM_CALC")
agent.sensorConfig = agent.newSensorConfig()
// Parse disk usage cache duration (e.g., "15m", "1h") to avoid waking sleeping disks
if diskUsageCache, exists := GetEnv("DISK_USAGE_CACHE"); exists {
if duration, err := time.ParseDuration(diskUsageCache); err == nil {
agent.diskUsageCacheDuration = duration
slog.Info("DISK_USAGE_CACHE", "duration", duration)
} else {
slog.Warn("Invalid DISK_USAGE_CACHE", "err", err)
}
}
// Set up slog with a log level determined by the LOG_LEVEL env var
if logLevelStr, exists := GetEnv("LOG_LEVEL"); exists {
switch strings.ToLower(logLevelStr) {
@@ -84,8 +100,21 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
slog.Debug(beszel.Version)
// initialize docker manager
agent.dockerManager = newDockerManager()
// initialize system info
agent.initializeSystemInfo()
agent.refreshSystemDetails()
// SMART_INTERVAL env var to update smart data at this interval
if smartIntervalEnv, exists := GetEnv("SMART_INTERVAL"); exists {
if duration, err := time.ParseDuration(smartIntervalEnv); err == nil && duration > 0 {
agent.systemDetails.SmartInterval = duration
slog.Info("SMART_INTERVAL", "duration", duration)
} else {
slog.Warn("Invalid SMART_INTERVAL", "err", err)
}
}
// initialize connection manager
agent.connectionManager = newConnectionManager(agent)
@@ -99,9 +128,6 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// initialize net io stats
agent.initializeNetIoStats()
// initialize docker manager
agent.dockerManager = newDockerManager(agent)
agent.systemdManager, err = newSystemdManager()
if err != nil {
slog.Debug("Systemd", "err", err)
@@ -120,7 +146,7 @@ func NewAgent(dataDir ...string) (agent *Agent, err error) {
// if debugging, print stats
if agent.debug {
slog.Debug("Stats", "data", agent.gatherStats(0))
slog.Debug("Stats", "data", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true}))
}
return agent, nil
@@ -135,10 +161,11 @@ func GetEnv(key string) (value string, exists bool) {
return os.LookupEnv(key)
}
func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
func (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {
a.Lock()
defer a.Unlock()
cacheTimeMs := options.CacheTimeMs
data, isCached := a.cache.Get(cacheTimeMs)
if isCached {
slog.Debug("Cached data", "cacheTimeMs", cacheTimeMs)
@@ -149,6 +176,12 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
Stats: a.getSystemStats(cacheTimeMs),
Info: a.systemInfo,
}
// Include static system details only when requested
if options.IncludeDetails {
data.Details = &a.systemDetails
}
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
if a.dockerManager != nil {
@@ -161,8 +194,15 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
}
// skip updating systemd services if cache time is not the default 60sec interval
if a.systemdManager != nil && cacheTimeMs == 60_000 && a.systemdManager.hasFreshStats {
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
if a.systemdManager != nil && cacheTimeMs == 60_000 {
totalCount := uint16(a.systemdManager.getServiceStatsCount())
if totalCount > 0 {
numFailed := a.systemdManager.getFailedServiceCount()
data.Info.Services = []uint16{totalCount, numFailed}
}
if a.systemdManager.hasFreshStats {
data.SystemdServices = a.systemdManager.getServiceStats(nil, false)
}
}
data.Stats.ExtraFs = make(map[string]*system.FsStats)
@@ -204,8 +244,9 @@ func (a *Agent) getFingerprint() string {
// if no fingerprint is found, generate one
fingerprint, err := host.HostID()
if err != nil || fingerprint == "" {
fingerprint = a.systemInfo.Hostname + a.systemInfo.CpuModel
// we ignore a commonly known "product_uuid" known not to be unique
if err != nil || fingerprint == "" || fingerprint == "03000200-0400-0500-0006-000700080009" {
fingerprint = a.systemDetails.Hostname + a.systemDetails.CpuModel
}
// hash fingerprint

View File

@@ -22,7 +22,7 @@ func createTestCacheData() *system.CombinedData {
DiskTotal: 100000,
},
Info: system.Info{
Hostname: "test-host",
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{
{
@@ -128,7 +128,7 @@ func TestCacheMultipleIntervals(t *testing.T) {
Mem: 16384,
},
Info: system.Info{
Hostname: "test-host-2",
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{},
}
@@ -171,7 +171,7 @@ func TestCacheOverwrite(t *testing.T) {
Mem: 32768,
},
Info: system.Info{
Hostname: "updated-host",
AgentVersion: "0.12.0",
},
Containers: []*container.Stats{},
}

View File

@@ -201,7 +201,7 @@ func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.R
if authRequest.NeedSysInfo {
response.Name, _ = GetEnv("SYSTEM_NAME")
response.Hostname = client.agent.systemInfo.Hostname
response.Hostname = client.agent.systemDetails.Hostname
serverAddr := client.agent.connectionManager.serverOptions.Addr
_, response.Port, _ = net.SplitHostPort(serverAddr)
}

View File

@@ -95,6 +95,9 @@ func (a *Agent) initializeDiskInfo() {
}
}
// Get the appropriate root mount point for this system
rootMountPoint := a.getRootMountPoint()
// Use FILESYSTEM env var to find root filesystem
if filesystem != "" {
for _, p := range partitions {
@@ -138,7 +141,7 @@ func (a *Agent) initializeDiskInfo() {
for _, p := range partitions {
// fmt.Println(p.Device, p.Mountpoint)
// Binary root fallback or docker root fallback
if !hasRoot && (p.Mountpoint == "/" || (p.Mountpoint == "/etc/hosts" && strings.HasPrefix(p.Device, "/dev"))) {
if !hasRoot && (p.Mountpoint == rootMountPoint || (p.Mountpoint == "/etc/hosts" && strings.HasPrefix(p.Device, "/dev"))) {
fs, match := findIoDevice(filepath.Base(p.Device), diskIoCounters, a.fsStats)
if match {
addFsStat(fs, p.Mountpoint, true)
@@ -174,8 +177,8 @@ func (a *Agent) initializeDiskInfo() {
// If no root filesystem set, use fallback
if !hasRoot {
rootDevice, _ := findIoDevice(filepath.Base(filesystem), diskIoCounters, a.fsStats)
slog.Info("Root disk", "mountpoint", "/", "io", rootDevice)
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: "/"}
slog.Info("Root disk", "mountpoint", rootMountPoint, "io", rootDevice)
a.fsStats[rootDevice] = &system.FsStats{Root: true, Mountpoint: rootMountPoint}
}
a.initializeDiskIoStats(diskIoCounters)
@@ -222,8 +225,19 @@ func (a *Agent) initializeDiskIoStats(diskIoCounters map[string]disk.IOCountersS
// Updates disk usage statistics for all monitored filesystems
func (a *Agent) updateDiskUsage(systemStats *system.Stats) {
// Check if we should skip extra filesystem collection to avoid waking sleeping disks.
// Root filesystem is always updated since it can't be sleeping while the agent runs.
// Always collect on first call (lastDiskUsageUpdate is zero) or if caching is disabled.
cacheExtraFs := a.diskUsageCacheDuration > 0 &&
!a.lastDiskUsageUpdate.IsZero() &&
time.Since(a.lastDiskUsageUpdate) < a.diskUsageCacheDuration
// disk usage
for _, stats := range a.fsStats {
// Skip non-root filesystems if caching is active
if cacheExtraFs && !stats.Root {
continue
}
if d, err := disk.Usage(stats.Mountpoint); err == nil {
stats.DiskTotal = bytesToGigabytes(d.Total)
stats.DiskUsed = bytesToGigabytes(d.Used)
@@ -241,6 +255,11 @@ func (a *Agent) updateDiskUsage(systemStats *system.Stats) {
stats.TotalWrite = 0
}
}
// Update the last disk usage update time when we've collected extra filesystems
if !cacheExtraFs {
a.lastDiskUsageUpdate = time.Now()
}
}
// Updates disk I/O statistics for all monitored filesystems
@@ -312,3 +331,32 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
}
}
}
// getRootMountPoint returns the appropriate root mount point for the system
// For immutable systems like Fedora Silverblue, it returns /sysroot instead of /
func (a *Agent) getRootMountPoint() string {
// 1. Check if /etc/os-release contains indicators of an immutable system
if osReleaseContent, err := os.ReadFile("/etc/os-release"); err == nil {
content := string(osReleaseContent)
if strings.Contains(content, "fedora") && strings.Contains(content, "silverblue") ||
strings.Contains(content, "coreos") ||
strings.Contains(content, "flatcar") ||
strings.Contains(content, "rhel-atomic") ||
strings.Contains(content, "centos-atomic") {
// Verify that /sysroot exists before returning it
if _, err := os.Stat("/sysroot"); err == nil {
return "/sysroot"
}
}
}
// 2. Check if /run/ostree is present (ostree-based systems like Silverblue)
if _, err := os.Stat("/run/ostree"); err == nil {
// Verify that /sysroot exists before returning it
if _, err := os.Stat("/sysroot"); err == nil {
return "/sysroot"
}
}
return "/"
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/disk"
@@ -233,3 +234,86 @@ func TestExtraFsKeyGeneration(t *testing.T) {
})
}
}
func TestDiskUsageCaching(t *testing.T) {
t.Run("caching disabled updates all filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 0, // caching disabled
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// Both should be updated (non-zero values from disk.Usage)
// Root stats should be populated in systemStats
assert.True(t, agent.lastDiskUsageUpdate.IsZero() || !agent.lastDiskUsageUpdate.IsZero(),
"lastDiskUsageUpdate should be set when caching is disabled")
})
t.Run("caching enabled always updates root filesystem", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/", DiskTotal: 100, DiskUsed: 50},
"sdb": {Root: false, Mountpoint: "/mnt/storage", DiskTotal: 200, DiskUsed: 100},
},
diskUsageCacheDuration: 1 * time.Hour,
lastDiskUsageUpdate: time.Now(), // cache is fresh
}
// Store original extra fs values
originalExtraTotal := agent.fsStats["sdb"].DiskTotal
originalExtraUsed := agent.fsStats["sdb"].DiskUsed
var stats system.Stats
agent.updateDiskUsage(&stats)
// Root should be updated (systemStats populated from disk.Usage call)
// We can't easily check if disk.Usage was called, but we verify the flow works
// Extra filesystem should retain cached values (not reset)
assert.Equal(t, originalExtraTotal, agent.fsStats["sdb"].DiskTotal,
"extra filesystem DiskTotal should be unchanged when cached")
assert.Equal(t, originalExtraUsed, agent.fsStats["sdb"].DiskUsed,
"extra filesystem DiskUsed should be unchanged when cached")
})
t.Run("first call always updates all filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 1 * time.Hour,
// lastDiskUsageUpdate is zero (first call)
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// After first call, lastDiskUsageUpdate should be set
assert.False(t, agent.lastDiskUsageUpdate.IsZero(),
"lastDiskUsageUpdate should be set after first call")
})
t.Run("expired cache updates extra filesystems", func(t *testing.T) {
agent := &Agent{
fsStats: map[string]*system.FsStats{
"sda": {Root: true, Mountpoint: "/"},
"sdb": {Root: false, Mountpoint: "/mnt/storage"},
},
diskUsageCacheDuration: 1 * time.Millisecond,
lastDiskUsageUpdate: time.Now().Add(-1 * time.Second), // cache expired
}
var stats system.Stats
agent.updateDiskUsage(&stats)
// lastDiskUsageUpdate should be refreshed since cache expired
assert.True(t, time.Since(agent.lastDiskUsageUpdate) < time.Second,
"lastDiskUsageUpdate should be refreshed when cache expires")
})
}

View File

@@ -14,6 +14,7 @@ import (
"net/url"
"os"
"path"
"regexp"
"strings"
"sync"
"time"
@@ -24,6 +25,10 @@ import (
"github.com/blang/semver"
)
// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)
// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
const (
// Docker API timeout in milliseconds
dockerTimeoutMs = 2100
@@ -55,6 +60,7 @@ type dockerManager struct {
decoder *json.Decoder // Reusable JSON decoder that reads from buf
apiStats *container.ApiStats // Reusable API stats object
excludeContainers []string // Patterns to exclude containers by name
usingPodman bool // Whether the Docker Engine API is running on Podman
// Cache-time-aware tracking for CPU stats (similar to cpu.go)
// Maps cache time intervals to container-specific CPU usage tracking
@@ -473,7 +479,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
}
// Creates a new http client for Docker or Podman API
func newDockerManager(a *Agent) *dockerManager {
func newDockerManager() *dockerManager {
dockerHost, exists := GetEnv("DOCKER_HOST")
if exists {
// return nil if set to empty string
@@ -559,7 +565,7 @@ func newDockerManager(a *Agent) *dockerManager {
// If using podman, return client
if strings.Contains(dockerHost, "podman") {
a.systemInfo.Podman = true
manager.usingPodman = true
manager.goodDockerVersion = true
return manager
}
@@ -692,13 +698,17 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin
return "", err
}
return builder.String(), nil
// Strip ANSI escape sequences from logs for clean display in web UI
logs := builder.String()
if strings.Contains(logs, "\x1b") {
logs = ansiEscapePattern.ReplaceAllString(logs, "")
}
return logs, nil
}
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
const headerSize = 8
var header [headerSize]byte
buf := make([]byte, 0, dockerLogsTail*200)
totalBytesRead := 0
for {
@@ -722,36 +732,37 @@ func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error {
// Check if reading this frame would exceed total log size limit
if totalBytesRead+int(frameLen) > maxTotalLogSize {
// Read and discard remaining data to avoid blocking
_, _ = io.Copy(io.Discard, io.LimitReader(reader, int64(frameLen)))
_, _ = io.CopyN(io.Discard, reader, int64(frameLen))
slog.Debug("Truncating logs: limit reached", "read", totalBytesRead, "limit", maxTotalLogSize)
return nil
}
buf = allocateBuffer(buf, int(frameLen))
if _, err := io.ReadFull(reader, buf[:frameLen]); err != nil {
n, err := io.CopyN(builder, reader, int64(frameLen))
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
if len(buf) > 0 {
builder.Write(buf[:min(int(frameLen), len(buf))])
}
return nil
}
return err
}
builder.Write(buf[:frameLen])
totalBytesRead += int(frameLen)
totalBytesRead += int(n)
}
}
func allocateBuffer(current []byte, needed int) []byte {
if cap(current) >= needed {
return current[:needed]
// GetHostInfo fetches the system info from Docker
func (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {
resp, err := dm.client.Get("http://localhost/info")
if err != nil {
return info, err
}
return make([]byte, needed)
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return info, err
}
return info, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
func (dm *dockerManager) IsPodman() bool {
return dm.usingPodman
}

View File

@@ -802,6 +802,24 @@ func TestNetworkRateCalculationFormula(t *testing.T) {
}
}
func TestGetHostInfo(t *testing.T) {
data, err := os.ReadFile("test-data/system_info.json")
require.NoError(t, err)
var info container.HostInfo
err = json.Unmarshal(data, &info)
require.NoError(t, err)
assert.Equal(t, "6.8.0-31-generic", info.KernelVersion)
assert.Equal(t, "Ubuntu 24.04 LTS", info.OperatingSystem)
// assert.Equal(t, "24.04", info.OSVersion)
// assert.Equal(t, "linux", info.OSType)
// assert.Equal(t, "x86_64", info.Architecture)
assert.EqualValues(t, 4, info.NCPU)
assert.EqualValues(t, 2095882240, info.MemTotal)
// assert.Equal(t, "27.0.1", info.ServerVersion)
}
func TestDeltaTrackerCacheTimeIsolation(t *testing.T) {
// Test that different cache times have separate DeltaTracker instances
dm := &dockerManager{
@@ -1053,53 +1071,6 @@ func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
})
}
func TestAllocateBuffer(t *testing.T) {
tests := []struct {
name string
currentCap int
needed int
expectedCap int
shouldRealloc bool
}{
{
name: "buffer has enough capacity",
currentCap: 1024,
needed: 512,
expectedCap: 1024,
shouldRealloc: false,
},
{
name: "buffer needs reallocation",
currentCap: 512,
needed: 1024,
expectedCap: 1024,
shouldRealloc: true,
},
{
name: "buffer needs exact size",
currentCap: 1024,
needed: 1024,
expectedCap: 1024,
shouldRealloc: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
current := make([]byte, 0, tt.currentCap)
result := allocateBuffer(current, tt.needed)
assert.Equal(t, tt.needed, len(result))
assert.GreaterOrEqual(t, cap(result), tt.expectedCap)
if tt.shouldRealloc {
// If reallocation was needed, capacity should be at least the needed size
assert.GreaterOrEqual(t, cap(result), tt.needed)
}
})
}
}
func TestShouldExcludeContainer(t *testing.T) {
tests := []struct {
name string
@@ -1203,3 +1174,59 @@ func TestShouldExcludeContainer(t *testing.T) {
})
}
}
func TestAnsiEscapePattern(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no ANSI codes",
input: "Hello, World!",
expected: "Hello, World!",
},
{
name: "simple color code",
input: "\x1b[34mINFO\x1b[0m client mode",
expected: "INFO client mode",
},
{
name: "multiple color codes",
input: "\x1b[31mERROR\x1b[0m: \x1b[33mWarning\x1b[0m message",
expected: "ERROR: Warning message",
},
{
name: "bold and color",
input: "\x1b[1;32mSUCCESS\x1b[0m",
expected: "SUCCESS",
},
{
name: "cursor movement codes",
input: "Line 1\x1b[KLine 2",
expected: "Line 1Line 2",
},
{
name: "256 color code",
input: "\x1b[38;5;196mRed text\x1b[0m",
expected: "Red text",
},
{
name: "RGB/truecolor code",
input: "\x1b[38;2;255;0;0mRed text\x1b[0m",
expected: "Red text",
},
{
name: "mixed content with newlines",
input: "\x1b[34m2024-01-01 12:00:00\x1b[0m INFO Starting\n\x1b[31m2024-01-01 12:00:01\x1b[0m ERROR Failed",
expected: "2024-01-01 12:00:00 INFO Starting\n2024-01-01 12:00:01 ERROR Failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ansiEscapePattern.ReplaceAllString(tt.input, "")
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -44,6 +44,7 @@ type GPUManager struct {
rocmSmi bool
tegrastats bool
intelGpuStats bool
nvml bool
GpuDataMap map[string]*system.GPUData
// lastAvgData stores the last calculated averages for each GPU
// Used when a collection happens before new data arrives (Count == 0)
@@ -297,8 +298,13 @@ func (gm *GPUManager) calculateGPUAverage(id string, gpu *system.GPUData, cacheK
currentCount := uint32(gpu.Count)
deltaCount := gm.calculateDeltaCount(currentCount, lastSnapshot)
// If no new data arrived, use last known average
// If no new data arrived
if deltaCount == 0 {
// If GPU appears suspended (instantaneous values are 0), return zero values
// Otherwise return last known average for temporary collection gaps
if gpu.Temperature == 0 && gpu.MemoryUsed == 0 {
return system.GPUData{Name: gpu.Name}
}
return gm.lastAvgData[id] // zero value if not found
}
@@ -396,7 +402,7 @@ func (gm *GPUManager) detectGPUs() error {
if _, err := exec.LookPath(intelGpuStatsCmd); err == nil {
gm.intelGpuStats = true
}
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelGpuStats {
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats || gm.intelGpuStats || gm.nvml {
return nil
}
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, tegrastats, or intel_gpu_top")
@@ -467,7 +473,20 @@ func NewGPUManager() (*GPUManager, error) {
gm.GpuDataMap = make(map[string]*system.GPUData)
if gm.nvidiaSmi {
gm.startCollector(nvidiaSmiCmd)
if nvml, _ := GetEnv("NVML"); nvml == "true" {
gm.nvml = true
gm.nvidiaSmi = false
collector := &nvmlCollector{gm: &gm}
if err := collector.init(); err == nil {
go collector.start()
} else {
slog.Warn("Failed to initialize NVML, falling back to nvidia-smi", "err", err)
gm.nvidiaSmi = true
gm.startCollector(nvidiaSmiCmd)
}
} else {
gm.startCollector(nvidiaSmiCmd)
}
}
if gm.rocmSmi {
gm.startCollector(rocmSmiCmd)

210
agent/gpu_nvml.go Normal file
View File

@@ -0,0 +1,210 @@
package agent
import (
"fmt"
"strings"
"time"
"unsafe"
"github.com/ebitengine/purego"
"github.com/henrygd/beszel/internal/entities/system"
"golang.org/x/exp/slog"
)
// NVML constants and types
const (
nvmlSuccess int = 0
)
type nvmlDevice uintptr
type nvmlReturn int
type nvmlMemoryV1 struct {
Total uint64
Free uint64
Used uint64
}
type nvmlMemoryV2 struct {
Version uint32
Total uint64
Reserved uint64
Free uint64
Used uint64
}
type nvmlUtilization struct {
Gpu uint32
Memory uint32
}
type nvmlPciInfo struct {
BusId [16]byte
Domain uint32
Bus uint32
Device uint32
PciDeviceId uint32
PciSubSystemId uint32
}
// NVML function signatures
var (
nvmlInit func() nvmlReturn
nvmlShutdown func() nvmlReturn
nvmlDeviceGetCount func(count *uint32) nvmlReturn
nvmlDeviceGetHandleByIndex func(index uint32, device *nvmlDevice) nvmlReturn
nvmlDeviceGetName func(device nvmlDevice, name *byte, length uint32) nvmlReturn
nvmlDeviceGetMemoryInfo func(device nvmlDevice, memory uintptr) nvmlReturn
nvmlDeviceGetUtilizationRates func(device nvmlDevice, utilization *nvmlUtilization) nvmlReturn
nvmlDeviceGetTemperature func(device nvmlDevice, sensorType int, temp *uint32) nvmlReturn
nvmlDeviceGetPowerUsage func(device nvmlDevice, power *uint32) nvmlReturn
nvmlDeviceGetPciInfo func(device nvmlDevice, pci *nvmlPciInfo) nvmlReturn
nvmlErrorString func(result nvmlReturn) string
)
type nvmlCollector struct {
gm *GPUManager
lib uintptr
devices []nvmlDevice
bdfs []string
isV2 bool
}
func (c *nvmlCollector) init() error {
slog.Debug("NVML: Initializing")
libPath := getNVMLPath()
lib, err := openLibrary(libPath)
if err != nil {
return fmt.Errorf("failed to load %s: %w", libPath, err)
}
c.lib = lib
purego.RegisterLibFunc(&nvmlInit, lib, "nvmlInit")
purego.RegisterLibFunc(&nvmlShutdown, lib, "nvmlShutdown")
purego.RegisterLibFunc(&nvmlDeviceGetCount, lib, "nvmlDeviceGetCount")
purego.RegisterLibFunc(&nvmlDeviceGetHandleByIndex, lib, "nvmlDeviceGetHandleByIndex")
purego.RegisterLibFunc(&nvmlDeviceGetName, lib, "nvmlDeviceGetName")
// Try to get v2 memory info, fallback to v1 if not available
if hasSymbol(lib, "nvmlDeviceGetMemoryInfo_v2") {
c.isV2 = true
purego.RegisterLibFunc(&nvmlDeviceGetMemoryInfo, lib, "nvmlDeviceGetMemoryInfo_v2")
} else {
purego.RegisterLibFunc(&nvmlDeviceGetMemoryInfo, lib, "nvmlDeviceGetMemoryInfo")
}
purego.RegisterLibFunc(&nvmlDeviceGetUtilizationRates, lib, "nvmlDeviceGetUtilizationRates")
purego.RegisterLibFunc(&nvmlDeviceGetTemperature, lib, "nvmlDeviceGetTemperature")
purego.RegisterLibFunc(&nvmlDeviceGetPowerUsage, lib, "nvmlDeviceGetPowerUsage")
purego.RegisterLibFunc(&nvmlDeviceGetPciInfo, lib, "nvmlDeviceGetPciInfo")
purego.RegisterLibFunc(&nvmlErrorString, lib, "nvmlErrorString")
if ret := nvmlInit(); ret != nvmlReturn(nvmlSuccess) {
return fmt.Errorf("nvmlInit failed: %v", ret)
}
var count uint32
if ret := nvmlDeviceGetCount(&count); ret != nvmlReturn(nvmlSuccess) {
return fmt.Errorf("nvmlDeviceGetCount failed: %v", ret)
}
for i := uint32(0); i < count; i++ {
var device nvmlDevice
if ret := nvmlDeviceGetHandleByIndex(i, &device); ret == nvmlReturn(nvmlSuccess) {
c.devices = append(c.devices, device)
// Get BDF for power state check
var pci nvmlPciInfo
if ret := nvmlDeviceGetPciInfo(device, &pci); ret == nvmlReturn(nvmlSuccess) {
busID := string(pci.BusId[:])
if idx := strings.Index(busID, "\x00"); idx != -1 {
busID = busID[:idx]
}
c.bdfs = append(c.bdfs, strings.ToLower(busID))
} else {
c.bdfs = append(c.bdfs, "")
}
}
}
return nil
}
func (c *nvmlCollector) start() {
defer nvmlShutdown()
ticker := time.Tick(3 * time.Second)
for range ticker {
c.collect()
}
}
func (c *nvmlCollector) collect() {
c.gm.Lock()
defer c.gm.Unlock()
for i, device := range c.devices {
id := fmt.Sprintf("%d", i)
bdf := c.bdfs[i]
// Update GPUDataMap
if _, ok := c.gm.GpuDataMap[id]; !ok {
var nameBuf [64]byte
if ret := nvmlDeviceGetName(device, &nameBuf[0], 64); ret != nvmlReturn(nvmlSuccess) {
continue
}
name := string(nameBuf[:strings.Index(string(nameBuf[:]), "\x00")])
name = strings.TrimPrefix(name, "NVIDIA ")
c.gm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, " Laptop GPU")}
}
gpu := c.gm.GpuDataMap[id]
if bdf != "" && !c.isGPUActive(bdf) {
slog.Debug("NVML: GPU is suspended, skipping", "bdf", bdf)
gpu.Temperature = 0
gpu.MemoryUsed = 0
continue
}
// Utilization
var utilization nvmlUtilization
if ret := nvmlDeviceGetUtilizationRates(device, &utilization); ret != nvmlReturn(nvmlSuccess) {
slog.Debug("NVML: Utilization failed (GPU likely suspended)", "bdf", bdf, "ret", ret)
gpu.Temperature = 0
gpu.MemoryUsed = 0
continue
}
slog.Debug("NVML: Collecting data for GPU", "bdf", bdf)
// Temperature
var temp uint32
nvmlDeviceGetTemperature(device, 0, &temp) // 0 is NVML_TEMPERATURE_GPU
// Memory
var usedMem, totalMem uint64
if c.isV2 {
var memory nvmlMemoryV2
memory.Version = 0x02000028 // (2 << 24) | 40 bytes
nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory)))
usedMem = memory.Used
totalMem = memory.Total
} else {
var memory nvmlMemoryV1
nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory)))
usedMem = memory.Used
totalMem = memory.Total
}
// Power
var power uint32
nvmlDeviceGetPowerUsage(device, &power)
gpu.Temperature = float64(temp)
gpu.MemoryUsed = float64(usedMem) / 1024 / 1024 / mebibytesInAMegabyte
gpu.MemoryTotal = float64(totalMem) / 1024 / 1024 / mebibytesInAMegabyte
gpu.Usage += float64(utilization.Gpu)
gpu.Power += float64(power) / 1000.0
gpu.Count++
slog.Debug("NVML: Collected data", "gpu", gpu)
}
}

57
agent/gpu_nvml_linux.go Normal file
View File

@@ -0,0 +1,57 @@
//go:build linux
package agent
import (
"os"
"path/filepath"
"strings"
"github.com/ebitengine/purego"
"golang.org/x/exp/slog"
)
func openLibrary(name string) (uintptr, error) {
return purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL)
}
func getNVMLPath() string {
return "libnvidia-ml.so.1"
}
func hasSymbol(lib uintptr, symbol string) bool {
_, err := purego.Dlsym(lib, symbol)
return err == nil
}
func (c *nvmlCollector) isGPUActive(bdf string) bool {
// runtime_status
statusPath := filepath.Join("/sys/bus/pci/devices", bdf, "power/runtime_status")
status, err := os.ReadFile(statusPath)
if err != nil {
slog.Debug("NVML: Can't read runtime_status", "bdf", bdf, "err", err)
return true // Assume active if we can't read status
}
statusStr := strings.TrimSpace(string(status))
if statusStr != "active" && statusStr != "resuming" {
slog.Debug("NVML: GPU not active", "bdf", bdf, "status", statusStr)
return false
}
// power_state (D0 check)
// Find any drm card device power_state
pstatePathPattern := filepath.Join("/sys/bus/pci/devices", bdf, "drm/card*/device/power_state")
matches, _ := filepath.Glob(pstatePathPattern)
if len(matches) > 0 {
pstate, err := os.ReadFile(matches[0])
if err == nil {
pstateStr := strings.TrimSpace(string(pstate))
if pstateStr != "D0" {
slog.Debug("NVML: GPU not in D0 state", "bdf", bdf, "pstate", pstateStr)
return false
}
}
}
return true
}

View File

@@ -0,0 +1,21 @@
//go:build !linux && !windows
package agent
import "fmt"
func openLibrary(name string) (uintptr, error) {
return 0, fmt.Errorf("nvml not supported on this platform")
}
func getNVMLPath() string {
return ""
}
func hasSymbol(lib uintptr, symbol string) bool {
return false
}
func (c *nvmlCollector) isGPUActive(bdf string) bool {
return true
}

25
agent/gpu_nvml_windows.go Normal file
View File

@@ -0,0 +1,25 @@
//go:build windows
package agent
import (
"golang.org/x/sys/windows"
)
func openLibrary(name string) (uintptr, error) {
handle, err := windows.LoadLibrary(name)
return uintptr(handle), err
}
func getNVMLPath() string {
return "nvml.dll"
}
func hasSymbol(lib uintptr, symbol string) bool {
_, err := windows.GetProcAddress(windows.Handle(lib), symbol)
return err == nil
}
func (c *nvmlCollector) isGPUActive(bdf string) bool {
return true
}

View File

@@ -94,7 +94,7 @@ func (h *GetDataHandler) Handle(hctx *HandlerContext) error {
var options common.DataRequestOptions
_ = cbor.Unmarshal(hctx.Request.Data, &options)
sysStats := hctx.Agent.gatherStats(options.CacheTimeMs)
sysStats := hctx.Agent.gatherStats(options)
return hctx.SendResponse(sysStats, hctx.RequestID)
}

View File

@@ -202,7 +202,7 @@ func (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMes
// handleLegacyStats serves the legacy one-shot stats payload for older hubs
func (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {
stats := a.gatherStats(60_000)
stats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000})
return a.writeToSession(w, stats, hubVersion)
}

View File

@@ -513,7 +513,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
err = json.Unmarshal([]byte(encodedData), &decodedJson)
assert.Error(t, err, "Should not be valid JSON data")
assert.Equal(t, testData.Info.Hostname, decodedCbor.Info.Hostname)
assert.Equal(t, testData.Details.Hostname, decodedCbor.Details.Hostname)
assert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)
} else {
// Should be JSON - try to decode as JSON
@@ -526,7 +526,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
assert.Error(t, err, "Should not be valid CBOR data")
// Verify the decoded JSON data matches our test data
assert.Equal(t, testData.Info.Hostname, decodedJson.Info.Hostname)
assert.Equal(t, testData.Details.Hostname, decodedJson.Details.Hostname)
assert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)
// Verify it looks like JSON (starts with '{' and contains readable field names)
@@ -550,13 +550,12 @@ func createTestCombinedData() *system.CombinedData {
DiskUsed: 549755813888, // 512GB
DiskPct: 50.0,
},
Details: &system.Details{
Hostname: "test-host",
},
Info: system.Info{
Hostname: "test-host",
Cores: 8,
CpuModel: "Test CPU Model",
Uptime: 3600,
AgentVersion: "0.12.0",
Os: system.Linux,
},
Containers: []*container.Stats{
{

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
@@ -430,7 +431,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
// Check if we have any existing data for this device
hasExistingData := sm.hasDataForDevice(deviceInfo.Name)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// Try with -n standby first if we have existing data
@@ -445,7 +446,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
return nil
}
// No cached data, need to collect initial data by bypassing standby
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
args = sm.smartctlArgs(deviceInfo, false)
cmd = exec.CommandContext(ctx2, sm.binPath, args...)
@@ -454,6 +455,34 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
hasValidData := sm.parseSmartOutput(deviceInfo, output)
// If NVMe controller path failed, try namespace path as fallback.
// NVMe controllers (/dev/nvme0) don't always support SMART queries. See github.com/henrygd/beszel/issues/1504
if !hasValidData && err != nil && isNvmeControllerPath(deviceInfo.Name) {
controllerPath := deviceInfo.Name
namespacePath := controllerPath + "n1"
if !sm.isExcludedDevice(namespacePath) {
deviceInfo.Name = namespacePath
ctx3, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel3()
args = sm.smartctlArgs(deviceInfo, false)
cmd = exec.CommandContext(ctx3, sm.binPath, args...)
output, err = cmd.CombinedOutput()
hasValidData = sm.parseSmartOutput(deviceInfo, output)
// Auto-exclude the controller path so future scans don't re-add it
if hasValidData {
sm.Lock()
if sm.excludedDevices == nil {
sm.excludedDevices = make(map[string]struct{})
}
sm.excludedDevices[controllerPath] = struct{}{}
sm.Unlock()
slog.Debug("auto-excluded NVMe controller path", "path", controllerPath)
}
}
}
if !hasValidData {
if err != nil {
slog.Info("smartctl failed", "device", deviceInfo.Name, "err", err)
@@ -957,6 +986,27 @@ func (sm *SmartManager) detectSmartctl() (string, error) {
return "", errors.New("smartctl not found")
}
// isNvmeControllerPath checks if the path matches an NVMe controller pattern
// like /dev/nvme0, /dev/nvme1, etc. (without namespace suffix like n1)
func isNvmeControllerPath(path string) bool {
base := filepath.Base(path)
if !strings.HasPrefix(base, "nvme") {
return false
}
suffix := strings.TrimPrefix(base, "nvme")
if suffix == "" {
return false
}
// Controller paths are just "nvme" + digits (e.g., nvme0, nvme1)
// Namespace paths have "n" after the controller number (e.g., nvme0n1)
for _, c := range suffix {
if c < '0' || c > '9' {
return false
}
}
return true
}
// NewSmartManager creates and initializes a new SmartManager
func NewSmartManager() (*SmartManager, error) {
sm := &SmartManager{

View File

@@ -780,3 +780,36 @@ func TestFilterExcludedDevices(t *testing.T) {
})
}
}
func TestIsNvmeControllerPath(t *testing.T) {
tests := []struct {
path string
expected bool
}{
// Controller paths (should return true)
{"/dev/nvme0", true},
{"/dev/nvme1", true},
{"/dev/nvme10", true},
{"nvme0", true},
// Namespace paths (should return false)
{"/dev/nvme0n1", false},
{"/dev/nvme1n1", false},
{"/dev/nvme0n1p1", false},
{"nvme0n1", false},
// Non-NVMe paths (should return false)
{"/dev/sda", false},
{"/dev/sda1", false},
{"/dev/hda", false},
{"", false},
{"/dev/nvme", false},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
result := isNvmeControllerPath(tt.path)
assert.Equal(t, tt.expected, result, "path: %s", tt.path)
})
}
}

View File

@@ -2,15 +2,18 @@ package agent
import (
"bufio"
"errors"
"fmt"
"log/slog"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/cpu"
@@ -27,41 +30,79 @@ type prevDisk struct {
}
// Sets initial / non-changing values about the host system
func (a *Agent) initializeSystemInfo() {
func (a *Agent) refreshSystemDetails() {
a.systemInfo.AgentVersion = beszel.Version
a.systemInfo.Hostname, _ = os.Hostname()
// get host info from Docker if available
var hostInfo container.HostInfo
if a.dockerManager != nil {
a.systemDetails.Podman = a.dockerManager.IsPodman()
hostInfo, _ = a.dockerManager.GetHostInfo()
}
a.systemDetails.Hostname, _ = os.Hostname()
if arch, err := host.KernelArch(); err == nil {
a.systemDetails.Arch = arch
} else {
a.systemDetails.Arch = runtime.GOARCH
}
platform, _, version, _ := host.PlatformInformation()
if platform == "darwin" {
a.systemInfo.KernelVersion = version
a.systemInfo.Os = system.Darwin
a.systemDetails.Os = system.Darwin
a.systemDetails.OsName = fmt.Sprintf("macOS %s", version)
} else if strings.Contains(platform, "indows") {
a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
a.systemInfo.Os = system.Windows
a.systemDetails.Os = system.Windows
a.systemDetails.OsName = strings.Replace(platform, "Microsoft ", "", 1)
a.systemDetails.Kernel = version
} else if platform == "freebsd" {
a.systemInfo.Os = system.Freebsd
a.systemInfo.KernelVersion = version
a.systemDetails.Os = system.Freebsd
a.systemDetails.Kernel, _ = host.KernelVersion()
if prettyName, err := getOsPrettyName(); err == nil {
a.systemDetails.OsName = prettyName
} else {
a.systemDetails.OsName = "FreeBSD"
}
} else {
a.systemInfo.Os = system.Linux
}
if a.systemInfo.KernelVersion == "" {
a.systemInfo.KernelVersion, _ = host.KernelVersion()
a.systemDetails.Os = system.Linux
a.systemDetails.OsName = hostInfo.OperatingSystem
if a.systemDetails.OsName == "" {
if prettyName, err := getOsPrettyName(); err == nil {
a.systemDetails.OsName = prettyName
} else {
a.systemDetails.OsName = platform
}
}
a.systemDetails.Kernel = hostInfo.KernelVersion
if a.systemDetails.Kernel == "" {
a.systemDetails.Kernel, _ = host.KernelVersion()
}
}
// cpu model
if info, err := cpu.Info(); err == nil && len(info) > 0 {
a.systemInfo.CpuModel = info[0].ModelName
a.systemDetails.CpuModel = info[0].ModelName
}
// cores / threads
a.systemInfo.Cores, _ = cpu.Counts(false)
if threads, err := cpu.Counts(true); err == nil {
if threads > 0 && threads < a.systemInfo.Cores {
// in lxc logical cores reflects container limits, so use that as cores if lower
a.systemInfo.Cores = threads
} else {
a.systemInfo.Threads = threads
cores, _ := cpu.Counts(false)
threads := hostInfo.NCPU
if threads == 0 {
threads, _ = cpu.Counts(true)
}
// in lxc, logical cores reflects container limits, so use that as cores if lower
if threads > 0 && threads < cores {
cores = threads
}
a.systemDetails.Cores = cores
a.systemDetails.Threads = threads
// total memory
a.systemDetails.MemoryTotal = hostInfo.MemTotal
if a.systemDetails.MemoryTotal == 0 {
if v, err := mem.VirtualMemory(); err == nil {
a.systemDetails.MemoryTotal = v.Total
}
}
@@ -195,21 +236,16 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
}
}
// update base system info
// update system info
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
a.systemInfo.Cpu = systemStats.Cpu
a.systemInfo.LoadAvg = systemStats.LoadAvg
// TODO: remove these in future release in favor of load avg array
a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0]
a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1]
a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2]
a.systemInfo.MemPct = systemStats.MemPct
a.systemInfo.DiskPct = systemStats.DiskPct
a.systemInfo.Battery = systemStats.Battery
a.systemInfo.Uptime, _ = host.Uptime()
// TODO: in future release, remove MB bandwidth values in favor of bytes
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
slog.Debug("sysinfo", "data", a.systemInfo)
a.systemInfo.Threads = a.systemDetails.Threads
return systemStats
}
@@ -239,3 +275,24 @@ func getARCSize() (uint64, error) {
return 0, fmt.Errorf("failed to parse size field")
}
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
func getOsPrettyName() (string, error) {
file, err := os.Open("/etc/os-release")
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if after, ok := strings.CutPrefix(line, "PRETTY_NAME="); ok {
value := after
value = strings.Trim(value, `"`)
return value, nil
}
}
return "", errors.New("pretty name not found")
}

View File

@@ -17,9 +17,7 @@ import (
"github.com/henrygd/beszel/internal/entities/systemd"
)
var (
errNoActiveTime = errors.New("no active time")
)
var errNoActiveTime = errors.New("no active time")
// systemdManager manages the collection of systemd service statistics.
type systemdManager struct {
@@ -27,18 +25,23 @@ type systemdManager struct {
serviceStatsMap map[string]*systemd.Service
isRunning bool
hasFreshStats bool
patterns []string
}
// newSystemdManager creates a new systemdManager.
func newSystemdManager() (*systemdManager, error) {
if skipSystemd, _ := GetEnv("SKIP_SYSTEMD"); skipSystemd == "true" {
return nil, nil
}
conn, err := dbus.NewSystemConnectionContext(context.Background())
if err != nil {
slog.Warn("Error connecting to systemd", "err", err, "ref", "https://beszel.dev/guide/systemd")
slog.Debug("Error connecting to systemd", "err", err, "ref", "https://beszel.dev/guide/systemd")
return nil, err
}
manager := &systemdManager{
serviceStatsMap: make(map[string]*systemd.Service),
patterns: getServicePatterns(),
}
manager.startWorker(conn)
@@ -62,6 +65,24 @@ func (sm *systemdManager) startWorker(conn *dbus.Conn) {
}()
}
// getServiceStatsCount returns the number of systemd services.
func (sm *systemdManager) getServiceStatsCount() int {
return len(sm.serviceStatsMap)
}
// getFailedServiceCount returns the number of systemd services in a failed state.
func (sm *systemdManager) getFailedServiceCount() uint16 {
sm.Lock()
defer sm.Unlock()
count := uint16(0)
for _, service := range sm.serviceStatsMap {
if service.State == systemd.StatusFailed {
count++
}
}
return count
}
// getServiceStats collects statistics for all running systemd services.
func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {
// start := time.Now()
@@ -91,7 +112,7 @@ func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*syst
defer conn.Close()
}
units, err := conn.ListUnitsByPatternsContext(context.Background(), []string{"loaded"}, []string{"*.service"})
units, err := conn.ListUnitsByPatternsContext(context.Background(), []string{"loaded"}, sm.patterns)
if err != nil {
slog.Error("Error listing systemd service units", "err", err)
return nil
@@ -227,3 +248,26 @@ func unescapeServiceName(name string) string {
}
return unescaped
}
// getServicePatterns returns the list of service patterns to match.
// It reads from the SERVICE_PATTERNS environment variable if set,
// otherwise defaults to "*service".
func getServicePatterns() []string {
patterns := []string{}
if envPatterns, _ := GetEnv("SERVICE_PATTERNS"); envPatterns != "" {
for pattern := range strings.SplitSeq(envPatterns, ",") {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
if !strings.HasSuffix(pattern, ".service") {
pattern += ".service"
}
patterns = append(patterns, pattern)
}
}
if len(patterns) == 0 {
patterns = []string{"*.service"}
}
return patterns
}

View File

@@ -23,6 +23,16 @@ func (sm *systemdManager) getServiceStats(conn any, refresh bool) []*systemd.Ser
return nil
}
// getServiceStatsCount returns 0 for non-linux systems.
func (sm *systemdManager) getServiceStatsCount() int {
return 0
}
// getFailedServiceCount returns 0 for non-linux systems.
func (sm *systemdManager) getFailedServiceCount() uint16 {
return 0
}
func (sm *systemdManager) getServiceDetails(string) (systemd.ServiceDetails, error) {
return nil, errors.New("systemd manager unavailable")
}

View File

@@ -3,6 +3,7 @@
package agent
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
@@ -46,3 +47,112 @@ func TestUnescapeServiceNameInvalid(t *testing.T) {
})
}
}
func TestGetServicePatterns(t *testing.T) {
tests := []struct {
name string
prefixedEnv string
unprefixedEnv string
expected []string
cleanupEnvVars bool
}{
{
name: "default when no env var set",
prefixedEnv: "",
unprefixedEnv: "",
expected: []string{"*.service"},
cleanupEnvVars: true,
},
{
name: "single pattern with prefixed env",
prefixedEnv: "nginx",
unprefixedEnv: "",
expected: []string{"nginx.service"},
cleanupEnvVars: true,
},
{
name: "single pattern with unprefixed env",
prefixedEnv: "",
unprefixedEnv: "nginx",
expected: []string{"nginx.service"},
cleanupEnvVars: true,
},
{
name: "prefixed env takes precedence",
prefixedEnv: "nginx",
unprefixedEnv: "apache",
expected: []string{"nginx.service"},
cleanupEnvVars: true,
},
{
name: "multiple patterns",
prefixedEnv: "nginx,apache,postgresql",
unprefixedEnv: "",
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
cleanupEnvVars: true,
},
{
name: "patterns with .service suffix",
prefixedEnv: "nginx.service,apache.service",
unprefixedEnv: "",
expected: []string{"nginx.service", "apache.service"},
cleanupEnvVars: true,
},
{
name: "mixed patterns with and without suffix",
prefixedEnv: "nginx.service,apache,postgresql.service",
unprefixedEnv: "",
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
cleanupEnvVars: true,
},
{
name: "patterns with whitespace",
prefixedEnv: " nginx , apache , postgresql ",
unprefixedEnv: "",
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
cleanupEnvVars: true,
},
{
name: "empty patterns are skipped",
prefixedEnv: "nginx,,apache, ,postgresql",
unprefixedEnv: "",
expected: []string{"nginx.service", "apache.service", "postgresql.service"},
cleanupEnvVars: true,
},
{
name: "wildcard pattern",
prefixedEnv: "*nginx*,*apache*",
unprefixedEnv: "",
expected: []string{"*nginx*.service", "*apache*.service"},
cleanupEnvVars: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clean up any existing env vars
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
os.Unsetenv("SERVICE_PATTERNS")
// Set up environment variables
if tt.prefixedEnv != "" {
os.Setenv("BESZEL_AGENT_SERVICE_PATTERNS", tt.prefixedEnv)
}
if tt.unprefixedEnv != "" {
os.Setenv("SERVICE_PATTERNS", tt.unprefixedEnv)
}
// Run the function
result := getServicePatterns()
// Verify results
assert.Equal(t, tt.expected, result, "Patterns should match expected values")
// Cleanup
if tt.cleanupEnvVars {
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
os.Unsetenv("SERVICE_PATTERNS")
}
})
}
}

View File

@@ -0,0 +1,17 @@
{
"ID": "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS",
"Containers": 14,
"ContainersRunning": 3,
"ContainersPaused": 1,
"ContainersStopped": 10,
"Images": 508,
"Driver": "overlay2",
"KernelVersion": "6.8.0-31-generic",
"OperatingSystem": "Ubuntu 24.04 LTS",
"OSVersion": "24.04",
"OSType": "linux",
"Architecture": "x86_64",
"NCPU": 4,
"MemTotal": 2095882240,
"ServerVersion": "27.0.1"
}

View File

@@ -6,7 +6,7 @@ import "github.com/blang/semver"
const (
// Version is the current version of the application.
Version = "0.16.0"
Version = "0.18.0-beta.1"
// AppName is the name of the application.
AppName = "beszel"
)

18
go.mod
View File

@@ -1,25 +1,27 @@
module github.com/henrygd/beszel
go 1.25.3
go 1.25.5
require (
github.com/blang/semver v3.5.1+incompatible
github.com/coreos/go-systemd/v22 v22.6.0
github.com/distatus/battery v0.11.0
github.com/ebitengine/purego v0.9.1
github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0
github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.12.0
github.com/nicholas-fedor/shoutrrr v0.12.1
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.32.0
github.com/pocketbase/pocketbase v0.34.0
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.44.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/crypto v0.45.0
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
golang.org/x/sys v0.38.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -31,14 +33,13 @@ require (
github.com/dolthub/maphash v0.1.0 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
@@ -57,12 +58,11 @@ require (
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.0 // indirect
modernc.org/sqlite v1.40.1 // indirect
)

36
go.sum
View File

@@ -51,15 +51,15 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -83,8 +83,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.12.0 h1:8mwJdfU+uBEybSymwQJMGl/grG7lvVUKbVSNxn3XvUI=
github.com/nicholas-fedor/shoutrrr v0.12.0/go.mod h1:WYiRalR4C43Qmd2zhPWGIFIxu633NB1hDM6Ap/DQcsA=
github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
github.com/nicholas-fedor/shoutrrr v0.12.1/go.mod h1:64qWuPpvTUv9ZppEoR6OdroiFmgf9w11YSaR0h9KZGg=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
@@ -94,8 +94,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.32.0 h1:2DskUUO06sjDeXzmi9NlU/xIa5OknuHAnDQk+ncsfvc=
github.com/pocketbase/pocketbase v0.32.0/go.mod h1:prwdJKQYTums5Nhy5eeqFR5qV2AIZlS8o2JD0k6qn5E=
github.com/pocketbase/pocketbase v0.34.0 h1:5W80PrGvkRYIMAIK90F7w031/hXgZVz1KSuCJqSpgJo=
github.com/pocketbase/pocketbase v0.34.0/go.mod h1:K/9z/Zb9PR9yW2Qyoc73jHV/EKT8cMTk9bQWyrzYlvI=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -127,15 +127,15 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
@@ -157,8 +157,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
@@ -191,8 +191,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -28,6 +28,7 @@ type AlertManager struct {
type AlertMessageData struct {
UserID string
SystemID string
Title string
Message string
Link string
@@ -48,6 +49,7 @@ type SystemAlertStats struct {
GPU map[string]SystemAlertGPUData `json:"g"`
Temperatures map[string]float32 `json:"t"`
LoadAvg [3]float64 `json:"la"`
Battery [2]uint8 `json:"bat"`
}
type SystemAlertGPUData struct {
@@ -77,7 +79,6 @@ var supportsTitle = map[string]struct{}{
"ifttt": {},
"join": {},
"lark": {},
"matrix": {},
"ntfy": {},
"opsgenie": {},
"pushbullet": {},
@@ -104,10 +105,84 @@ func NewAlertManager(app hubLike) *AlertManager {
func (am *AlertManager) bindEvents() {
am.hub.OnRecordAfterUpdateSuccess("alerts").BindFunc(updateHistoryOnAlertUpdate)
am.hub.OnRecordAfterDeleteSuccess("alerts").BindFunc(resolveHistoryOnAlertDelete)
am.hub.OnRecordAfterUpdateSuccess("smart_devices").BindFunc(am.handleSmartDeviceAlert)
}
// IsNotificationSilenced checks if a notification should be silenced based on configured quiet hours
func (am *AlertManager) IsNotificationSilenced(userID, systemID string) bool {
// Query for quiet hours windows that match this user and system
// Include both global windows (system is null/empty) and system-specific windows
var filter string
var params dbx.Params
if systemID == "" {
// If no systemID provided, only check global windows
filter = "user={:user} AND system=''"
params = dbx.Params{"user": userID}
} else {
// Check both global and system-specific windows
filter = "user={:user} AND (system='' OR system={:system})"
params = dbx.Params{
"user": userID,
"system": systemID,
}
}
quietHourWindows, err := am.hub.FindAllRecords("quiet_hours", dbx.NewExp(filter, params))
if err != nil || len(quietHourWindows) == 0 {
return false
}
now := time.Now().UTC()
for _, window := range quietHourWindows {
windowType := window.GetString("type")
start := window.GetDateTime("start").Time()
end := window.GetDateTime("end").Time()
if windowType == "daily" {
// For daily recurring windows, extract just the time portion and compare
// The start/end are stored as full datetime but we only care about HH:MM
startHour, startMin, _ := start.Clock()
endHour, endMin, _ := end.Clock()
nowHour, nowMin, _ := now.Clock()
// Convert to minutes since midnight for easier comparison
startMinutes := startHour*60 + startMin
endMinutes := endHour*60 + endMin
nowMinutes := nowHour*60 + nowMin
// Handle case where window crosses midnight
if endMinutes < startMinutes {
// Window crosses midnight (e.g., 23:00 - 01:00)
if nowMinutes >= startMinutes || nowMinutes < endMinutes {
return true
}
} else {
// Normal case (e.g., 09:00 - 17:00)
if nowMinutes >= startMinutes && nowMinutes < endMinutes {
return true
}
}
} else {
// One-time window: check if current time is within the date range
if (now.After(start) || now.Equal(start)) && now.Before(end) {
return true
}
}
}
return false
}
// SendAlert sends an alert to the user
func (am *AlertManager) SendAlert(data AlertMessageData) error {
// Check if alert is silenced
if am.IsNotificationSilenced(data.UserID, data.SystemID) {
am.hub.Logger().Info("Notification silenced", "user", data.UserID, "system", data.SystemID, "title", data.Title)
return nil
}
// get user settings
record, err := am.hub.FindFirstRecordByFilter(
"user_settings", "user={:user}",

View File

@@ -0,0 +1,387 @@
//go:build testing
// +build testing
package alerts_test
import (
"encoding/json"
"testing"
"time"
"github.com/henrygd/beszel/internal/entities/system"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestBatteryAlertLogic tests that battery alerts trigger when value drops BELOW threshold
// (opposite of other alerts like CPU, Memory, etc. which trigger when exceeding threshold)
func TestBatteryAlertLogic(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
require.NoError(t, err)
systemRecord := systems[0]
// Create a battery alert with threshold of 20% and min of 1 minute (immediate trigger)
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Battery",
"system": systemRecord.Id,
"user": user.Id,
"value": 20, // threshold: 20%
"min": 1, // 1 minute (immediate trigger for testing)
})
require.NoError(t, err)
// Verify alert is not triggered initially
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should not be triggered initially")
// Create system stats with battery at 50% (above threshold - should NOT trigger)
statsHigh := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{50, 1}, // 50% battery, discharging
}
statsHighJSON, _ := json.Marshal(statsHigh)
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
"system": systemRecord.Id,
"type": "1m",
"stats": string(statsHighJSON),
})
require.NoError(t, err)
// Create CombinedData for the alert handler
combinedDataHigh := &system.CombinedData{
Stats: statsHigh,
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Simulate system update time
systemRecord.Set("updated", time.Now().UTC())
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts with high battery
am := hub.GetAlertManager()
err = am.HandleSystemAlerts(systemRecord, combinedDataHigh)
require.NoError(t, err)
// Verify alert is still NOT triggered (battery 50% is above threshold 20%)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should NOT be triggered when battery (50%%) is above threshold (20%%)")
// Now create stats with battery at 15% (below threshold - should trigger)
statsLow := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{15, 1}, // 15% battery, discharging
}
statsLowJSON, _ := json.Marshal(statsLow)
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
"system": systemRecord.Id,
"type": "1m",
"stats": string(statsLowJSON),
})
require.NoError(t, err)
combinedDataLow := &system.CombinedData{
Stats: statsLow,
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Update system timestamp
systemRecord.Set("updated", time.Now().UTC())
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts with low battery
err = am.HandleSystemAlerts(systemRecord, combinedDataLow)
require.NoError(t, err)
// Wait for the alert to be processed
time.Sleep(20 * time.Millisecond)
// Verify alert IS triggered (battery 15% is below threshold 20%)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.True(t, batteryAlert.GetBool("triggered"), "Alert SHOULD be triggered when battery (15%%) drops below threshold (20%%)")
// Now test resolution: battery goes back above threshold
statsRecovered := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{25, 1}, // 25% battery, discharging
}
statsRecoveredJSON, _ := json.Marshal(statsRecovered)
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
"system": systemRecord.Id,
"type": "1m",
"stats": string(statsRecoveredJSON),
})
require.NoError(t, err)
combinedDataRecovered := &system.CombinedData{
Stats: statsRecovered,
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Update system timestamp
systemRecord.Set("updated", time.Now().UTC())
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts with recovered battery
err = am.HandleSystemAlerts(systemRecord, combinedDataRecovered)
require.NoError(t, err)
// Wait for the alert to be processed
time.Sleep(20 * time.Millisecond)
// Verify alert is now resolved (battery 25% is above threshold 20%)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should be resolved when battery (25%%) goes above threshold (20%%)")
}
// TestBatteryAlertNoBattery verifies that systems without battery data don't trigger alerts
func TestBatteryAlertNoBattery(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
require.NoError(t, err)
systemRecord := systems[0]
// Create a battery alert
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Battery",
"system": systemRecord.Id,
"user": user.Id,
"value": 20,
"min": 1,
})
require.NoError(t, err)
// Create stats with NO battery data (Battery[0] = 0)
statsNoBattery := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{0, 0}, // No battery
}
combinedData := &system.CombinedData{
Stats: statsNoBattery,
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Simulate system update time
systemRecord.Set("updated", time.Now().UTC())
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts
am := hub.GetAlertManager()
err = am.HandleSystemAlerts(systemRecord, combinedData)
require.NoError(t, err)
// Wait a moment for processing
time.Sleep(20 * time.Millisecond)
// Verify alert is NOT triggered (no battery data should skip the alert)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should NOT be triggered when system has no battery")
}
// TestBatteryAlertAveragedSamples tests battery alerts with min > 1 (averaging multiple samples)
// This ensures the inverted threshold logic works correctly across averaged time windows
func TestBatteryAlertAveragedSamples(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
require.NoError(t, err)
systemRecord := systems[0]
// Create a battery alert with threshold of 25% and min of 2 minutes (requires averaging)
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Battery",
"system": systemRecord.Id,
"user": user.Id,
"value": 25, // threshold: 25%
"min": 2, // 2 minutes - requires averaging
})
require.NoError(t, err)
// Verify alert is not triggered initially
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should not be triggered initially")
am := hub.GetAlertManager()
now := time.Now().UTC()
// Create system_stats records with low battery (below threshold)
// The alert has min=2 minutes, so alert.time = now - 2 minutes
// For the alert to be valid, alert.time must be AFTER the oldest record's created time
// So we need records older than (now - 2 min), plus records within the window
// Records at: now-3min (oldest, before window), now-90s, now-60s, now-30s
recordTimes := []time.Duration{
-180 * time.Second, // 3 min ago - this makes the oldest record before alert.time
-90 * time.Second,
-60 * time.Second,
-30 * time.Second,
}
for _, offset := range recordTimes {
statsLow := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{15, 1}, // 15% battery (below 25% threshold)
}
statsLowJSON, _ := json.Marshal(statsLow)
recordTime := now.Add(offset)
record, err := beszelTests.CreateRecord(hub, "system_stats", map[string]any{
"system": systemRecord.Id,
"type": "1m",
"stats": string(statsLowJSON),
})
require.NoError(t, err)
// Update created time to simulate historical records - use SetRaw with formatted string
record.SetRaw("created", recordTime.Format(types.DefaultDateLayout))
err = hub.SaveNoValidate(record)
require.NoError(t, err)
}
// Create combined data with low battery
combinedDataLow := &system.CombinedData{
Stats: system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{15, 1},
},
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Update system timestamp
systemRecord.Set("updated", now)
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts - should trigger because average battery is below threshold
err = am.HandleSystemAlerts(systemRecord, combinedDataLow)
require.NoError(t, err)
// Wait for alert processing
time.Sleep(20 * time.Millisecond)
// Verify alert IS triggered (average battery 15% is below threshold 25%)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.True(t, batteryAlert.GetBool("triggered"),
"Alert SHOULD be triggered when average battery (15%%) is below threshold (25%%) over min period")
// Now add records with high battery to test resolution
// Use a new time window 2 minutes later
newNow := now.Add(2 * time.Minute)
// Records need to span before the alert time window (newNow - 2 min)
recordTimesHigh := []time.Duration{
-180 * time.Second, // 3 min before newNow - makes oldest record before alert.time
-90 * time.Second,
-60 * time.Second,
-30 * time.Second,
}
for _, offset := range recordTimesHigh {
statsHigh := system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{50, 1}, // 50% battery (above 25% threshold)
}
statsHighJSON, _ := json.Marshal(statsHigh)
recordTime := newNow.Add(offset)
record, err := beszelTests.CreateRecord(hub, "system_stats", map[string]any{
"system": systemRecord.Id,
"type": "1m",
"stats": string(statsHighJSON),
})
require.NoError(t, err)
record.SetRaw("created", recordTime.Format(types.DefaultDateLayout))
err = hub.SaveNoValidate(record)
require.NoError(t, err)
}
// Create combined data with high battery
combinedDataHigh := &system.CombinedData{
Stats: system.Stats{
Cpu: 10,
MemPct: 30,
DiskPct: 40,
Battery: [2]uint8{50, 1},
},
Info: system.Info{
AgentVersion: "0.12.0",
Cpu: 10,
MemPct: 30,
DiskPct: 40,
},
}
// Update system timestamp to the new time window
systemRecord.Set("updated", newNow)
err = hub.SaveNoValidate(systemRecord)
require.NoError(t, err)
// Handle system alerts - should resolve because average battery is now above threshold
err = am.HandleSystemAlerts(systemRecord, combinedDataHigh)
require.NoError(t, err)
// Wait for alert processing
time.Sleep(20 * time.Millisecond)
// Verify alert is resolved (average battery 50% is above threshold 25%)
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
require.NoError(t, err)
assert.False(t, batteryAlert.GetBool("triggered"),
"Alert should be resolved when average battery (50%%) is above threshold (25%%) over min period")
}

View File

@@ -0,0 +1,426 @@
//go:build testing
// +build testing
package alerts_test
import (
"testing"
"testing/synctest"
"time"
"github.com/henrygd/beszel/internal/alerts"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/dbx"
"github.com/stretchr/testify/assert"
)
func TestAlertSilencedOneTime(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
// Create an alert
alert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "CPU",
"system": system.Id,
"user": user.Id,
"value": 80,
"min": 1,
})
assert.NoError(t, err)
// Create a one-time quiet hours window (current time - 1 hour to current time + 1 hour)
now := time.Now().UTC()
startTime := now.Add(-1 * time.Hour)
endTime := now.Add(1 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "one-time",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Test that alert is silenced
silenced := am.IsNotificationSilenced(user.Id, system.Id)
assert.True(t, silenced, "Alert should be silenced during active one-time window")
// Create a window that has already ended
pastStart := now.Add(-3 * time.Hour)
pastEnd := now.Add(-2 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "one-time",
"start": pastStart,
"end": pastEnd,
})
assert.NoError(t, err)
// Should still be silenced because of the first window
silenced = am.IsNotificationSilenced(user.Id, system.Id)
assert.True(t, silenced, "Alert should still be silenced (past window doesn't affect active window)")
// Clear all windows and create a future window
_, err = hub.DB().NewQuery("DELETE FROM quiet_hours").Execute()
assert.NoError(t, err)
futureStart := now.Add(2 * time.Hour)
futureEnd := now.Add(3 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "one-time",
"start": futureStart,
"end": futureEnd,
})
assert.NoError(t, err)
// Alert should NOT be silenced (window hasn't started yet)
silenced = am.IsNotificationSilenced(user.Id, system.Id)
assert.False(t, silenced, "Alert should not be silenced (window hasn't started)")
_ = alert
}
func TestAlertSilencedDaily(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Get current hour and create a window that includes current time
now := time.Now().UTC()
currentHour := now.Hour()
currentMin := now.Minute()
// Create a window from 1 hour ago to 1 hour from now
startHour := (currentHour - 1 + 24) % 24
endHour := (currentHour + 1) % 24
// Create times with just the hours/minutes we want (date doesn't matter for daily)
startTime := time.Date(2000, 1, 1, startHour, currentMin, 0, 0, time.UTC)
endTime := time.Date(2000, 1, 1, endHour, currentMin, 0, 0, time.UTC)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "daily",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// Alert should be silenced (current time is within the daily window)
silenced := am.IsNotificationSilenced(user.Id, system.Id)
assert.True(t, silenced, "Alert should be silenced during active daily window")
// Clear windows and create one that doesn't include current time
_, err = hub.DB().NewQuery("DELETE FROM quiet_hours").Execute()
assert.NoError(t, err)
// Create a window from 6-12 hours from now
futureStartHour := (currentHour + 6) % 24
futureEndHour := (currentHour + 12) % 24
startTime = time.Date(2000, 1, 1, futureStartHour, 0, 0, 0, time.UTC)
endTime = time.Date(2000, 1, 1, futureEndHour, 0, 0, 0, time.UTC)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "daily",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// Alert should NOT be silenced
silenced = am.IsNotificationSilenced(user.Id, system.Id)
assert.False(t, silenced, "Alert should not be silenced (outside daily window)")
}
func TestAlertSilencedDailyMidnightCrossing(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Create a window that crosses midnight: 22:00 - 02:00
startTime := time.Date(2000, 1, 1, 22, 0, 0, 0, time.UTC)
endTime := time.Date(2000, 1, 1, 2, 0, 0, 0, time.UTC)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "daily",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// Test with a time at 23:00 (should be silenced)
// We can't control the actual current time, but we can verify the logic
// by checking if the window was created correctly
windows, err := hub.FindAllRecords("quiet_hours", dbx.HashExp{
"user": user.Id,
"system": system.Id,
})
assert.NoError(t, err)
assert.Len(t, windows, 1, "Should have created 1 window")
window := windows[0]
assert.Equal(t, "daily", window.GetString("type"))
assert.Equal(t, 22, window.GetDateTime("start").Time().Hour())
assert.Equal(t, 2, window.GetDateTime("end").Time().Hour())
}
func TestAlertSilencedGlobal(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create multiple systems
systems, err := beszelTests.CreateSystems(hub, 3, user.Id, "up")
assert.NoError(t, err)
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Create a global quiet hours window (no system specified)
now := time.Now().UTC()
startTime := now.Add(-1 * time.Hour)
endTime := now.Add(1 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"type": "one-time",
"start": startTime,
"end": endTime,
// system field is empty/null for global windows
})
assert.NoError(t, err)
// All systems should be silenced
for _, system := range systems {
silenced := am.IsNotificationSilenced(user.Id, system.Id)
assert.True(t, silenced, "Alert should be silenced for system %s (global window)", system.Id)
}
// Even with a systemID that doesn't exist, should be silenced
silenced := am.IsNotificationSilenced(user.Id, "nonexistent-system")
assert.True(t, silenced, "Alert should be silenced for any system (global window)")
}
func TestAlertSilencedSystemSpecific(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create multiple systems
systems, err := beszelTests.CreateSystems(hub, 2, user.Id, "up")
assert.NoError(t, err)
system1 := systems[0]
system2 := systems[1]
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Create a system-specific quiet hours window for system1 only
now := time.Now().UTC()
startTime := now.Add(-1 * time.Hour)
endTime := now.Add(1 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system1.Id,
"type": "one-time",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// System1 should be silenced
silenced := am.IsNotificationSilenced(user.Id, system1.Id)
assert.True(t, silenced, "Alert should be silenced for system1")
// System2 should NOT be silenced
silenced = am.IsNotificationSilenced(user.Id, system2.Id)
assert.False(t, silenced, "Alert should not be silenced for system2")
}
func TestAlertSilencedMultiUser(t *testing.T) {
hub, _ := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create two users
user1, err := beszelTests.CreateUser(hub, "user1@example.com", "password")
assert.NoError(t, err)
user2, err := beszelTests.CreateUser(hub, "user2@example.com", "password")
assert.NoError(t, err)
// Create a system accessible to both users
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "shared-system",
"users": []string{user1.Id, user2.Id},
"host": "127.0.0.1",
})
assert.NoError(t, err)
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Create a quiet hours window for user1 only
now := time.Now().UTC()
startTime := now.Add(-1 * time.Hour)
endTime := now.Add(1 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user1.Id,
"system": system.Id,
"type": "one-time",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// User1 should be silenced
silenced := am.IsNotificationSilenced(user1.Id, system.Id)
assert.True(t, silenced, "Alert should be silenced for user1")
// User2 should NOT be silenced
silenced = am.IsNotificationSilenced(user2.Id, system.Id)
assert.False(t, silenced, "Alert should not be silenced for user2")
}
func TestAlertSilencedWithActualAlert(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
// Create a status alert
_, err = beszelTests.CreateRecord(hub, "alerts", map[string]any{
"name": "Status",
"system": system.Id,
"user": user.Id,
"min": 1,
})
assert.NoError(t, err)
// Create user settings with email
userSettings, err := hub.FindFirstRecordByFilter("user_settings", "user={:user}", dbx.Params{"user": user.Id})
if err != nil || userSettings == nil {
userSettings, err = beszelTests.CreateRecord(hub, "user_settings", map[string]any{
"user": user.Id,
"settings": map[string]any{
"emails": []string{"test@example.com"},
},
})
assert.NoError(t, err)
}
// Create a quiet hours window
now := time.Now().UTC()
startTime := now.Add(-1 * time.Hour)
endTime := now.Add(1 * time.Hour)
_, err = beszelTests.CreateRecord(hub, "quiet_hours", map[string]any{
"user": user.Id,
"system": system.Id,
"type": "one-time",
"start": startTime,
"end": endTime,
})
assert.NoError(t, err)
// Get initial email count
initialEmailCount := hub.TestMailer.TotalSend()
// Trigger an alert by setting system to down
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
// Wait for the alert to be processed (1 minute + buffer)
time.Sleep(time.Second * 75)
synctest.Wait()
// Check that no email was sent (because alert is silenced)
finalEmailCount := hub.TestMailer.TotalSend()
assert.Equal(t, initialEmailCount, finalEmailCount, "No emails should be sent when alert is silenced")
// Clear quiet hours windows
_, err = hub.DB().NewQuery("DELETE FROM quiet_hours").Execute()
assert.NoError(t, err)
// Reset system to up, then down again
system.Set("status", "up")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
time.Sleep(100 * time.Millisecond)
system.Set("status", "down")
err = hub.SaveNoValidate(system)
assert.NoError(t, err)
// Wait for the alert to be processed
time.Sleep(time.Second * 75)
synctest.Wait()
// Now an email should be sent
newEmailCount := hub.TestMailer.TotalSend()
assert.Greater(t, newEmailCount, finalEmailCount, "Email should be sent when not silenced")
})
}
func TestAlertSilencedNoWindows(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
assert.NoError(t, err)
system := systems[0]
// Get alert manager
am := alerts.NewAlertManager(hub)
defer am.StopWorker()
// Without any quiet hours windows, alert should NOT be silenced
silenced := am.IsNotificationSilenced(user.Id, system.Id)
assert.False(t, silenced, "Alert should not be silenced when no windows exist")
}

View File

@@ -0,0 +1,67 @@
package alerts
import (
"fmt"
"github.com/pocketbase/pocketbase/core"
)
// handleSmartDeviceAlert sends alerts when a SMART device state changes from PASSED to FAILED.
// This is automatic and does not require user opt-in.
func (am *AlertManager) handleSmartDeviceAlert(e *core.RecordEvent) error {
oldState := e.Record.Original().GetString("state")
newState := e.Record.GetString("state")
// Only alert when transitioning from PASSED to FAILED
if oldState != "PASSED" || newState != "FAILED" {
return e.Next()
}
systemID := e.Record.GetString("system")
if systemID == "" {
return e.Next()
}
// Fetch the system record to get the name and users
systemRecord, err := e.App.FindRecordById("systems", systemID)
if err != nil {
e.App.Logger().Error("Failed to find system for SMART alert", "err", err, "systemID", systemID)
return e.Next()
}
systemName := systemRecord.GetString("name")
deviceName := e.Record.GetString("name")
model := e.Record.GetString("model")
// Build alert message
title := fmt.Sprintf("SMART failure on %s: %s \U0001F534", systemName, deviceName)
var message string
if model != "" {
message = fmt.Sprintf("Disk %s (%s) SMART status changed to FAILED", deviceName, model)
} else {
message = fmt.Sprintf("Disk %s SMART status changed to FAILED", deviceName)
}
// Get users associated with the system
userIDs := systemRecord.GetStringSlice("users")
if len(userIDs) == 0 {
return e.Next()
}
// Send alert to each user
for _, userID := range userIDs {
if err := am.SendAlert(AlertMessageData{
UserID: userID,
SystemID: systemID,
Title: title,
Message: message,
Link: am.hub.MakeLink("system", systemID),
LinkText: "View " + systemName,
}); err != nil {
e.App.Logger().Error("Failed to send SMART alert", "err", err, "userID", userID)
}
}
return e.Next()
}

View File

@@ -0,0 +1,196 @@
//go:build testing
// +build testing
package alerts_test
import (
"testing"
"time"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/stretchr/testify/assert"
)
func TestSmartDeviceAlert(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system for the user
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system",
"users": []string{user.Id},
"host": "127.0.0.1",
})
assert.NoError(t, err)
// Create a smart_device with state PASSED
smartDevice, err := beszelTests.CreateRecord(hub, "smart_devices", map[string]any{
"system": system.Id,
"name": "/dev/sda",
"model": "Samsung SSD 970 EVO",
"state": "PASSED",
})
assert.NoError(t, err)
// Verify no emails sent initially
assert.Zero(t, hub.TestMailer.TotalSend(), "should have 0 emails sent initially")
// Re-fetch the record so PocketBase can properly track original values
smartDevice, err = hub.FindRecordById("smart_devices", smartDevice.Id)
assert.NoError(t, err)
// Update the smart device state to FAILED
smartDevice.Set("state", "FAILED")
err = hub.Save(smartDevice)
assert.NoError(t, err)
// Wait for the alert to be processed
time.Sleep(50 * time.Millisecond)
// Verify that an email was sent
assert.EqualValues(t, 1, hub.TestMailer.TotalSend(), "should have 1 email sent after state changed to FAILED")
// Check the email content
lastMessage := hub.TestMailer.LastMessage()
assert.Contains(t, lastMessage.Subject, "SMART failure on test-system")
assert.Contains(t, lastMessage.Subject, "/dev/sda")
assert.Contains(t, lastMessage.Text, "Samsung SSD 970 EVO")
assert.Contains(t, lastMessage.Text, "FAILED")
}
func TestSmartDeviceAlertNoAlertOnNonPassedToFailed(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system for the user
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system",
"users": []string{user.Id},
"host": "127.0.0.1",
})
assert.NoError(t, err)
// Create a smart_device with state UNKNOWN
smartDevice, err := beszelTests.CreateRecord(hub, "smart_devices", map[string]any{
"system": system.Id,
"name": "/dev/sda",
"model": "Samsung SSD 970 EVO",
"state": "UNKNOWN",
})
assert.NoError(t, err)
// Re-fetch the record so PocketBase can properly track original values
smartDevice, err = hub.FindRecordById("smart_devices", smartDevice.Id)
assert.NoError(t, err)
// Update the state from UNKNOWN to FAILED - should NOT trigger alert
smartDevice.Set("state", "FAILED")
err = hub.Save(smartDevice)
assert.NoError(t, err)
time.Sleep(50 * time.Millisecond)
// Verify no email was sent (only PASSED -> FAILED triggers alert)
assert.Zero(t, hub.TestMailer.TotalSend(), "should have 0 emails when changing from UNKNOWN to FAILED")
// Re-fetch the record again
smartDevice, err = hub.FindRecordById("smart_devices", smartDevice.Id)
assert.NoError(t, err)
// Update state from FAILED to PASSED - should NOT trigger alert
smartDevice.Set("state", "PASSED")
err = hub.Save(smartDevice)
assert.NoError(t, err)
time.Sleep(50 * time.Millisecond)
// Verify no email was sent
assert.Zero(t, hub.TestMailer.TotalSend(), "should have 0 emails when changing from FAILED to PASSED")
}
func TestSmartDeviceAlertMultipleUsers(t *testing.T) {
hub, user1 := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a second user
user2, err := beszelTests.CreateUser(hub, "test2@example.com", "password")
assert.NoError(t, err)
// Create user settings for the second user
_, err = beszelTests.CreateRecord(hub, "user_settings", map[string]any{
"user": user2.Id,
"settings": `{"emails":["test2@example.com"],"webhooks":[]}`,
})
assert.NoError(t, err)
// Create a system with both users
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "shared-system",
"users": []string{user1.Id, user2.Id},
"host": "127.0.0.1",
})
assert.NoError(t, err)
// Create a smart_device with state PASSED
smartDevice, err := beszelTests.CreateRecord(hub, "smart_devices", map[string]any{
"system": system.Id,
"name": "/dev/nvme0n1",
"model": "WD Black SN850",
"state": "PASSED",
})
assert.NoError(t, err)
// Re-fetch the record so PocketBase can properly track original values
smartDevice, err = hub.FindRecordById("smart_devices", smartDevice.Id)
assert.NoError(t, err)
// Update the smart device state to FAILED
smartDevice.Set("state", "FAILED")
err = hub.Save(smartDevice)
assert.NoError(t, err)
time.Sleep(50 * time.Millisecond)
// Verify that two emails were sent (one for each user)
assert.EqualValues(t, 2, hub.TestMailer.TotalSend(), "should have 2 emails sent for 2 users")
}
func TestSmartDeviceAlertWithoutModel(t *testing.T) {
hub, user := beszelTests.GetHubWithUser(t)
defer hub.Cleanup()
// Create a system for the user
system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "test-system",
"users": []string{user.Id},
"host": "127.0.0.1",
})
assert.NoError(t, err)
// Create a smart_device with state PASSED but no model
smartDevice, err := beszelTests.CreateRecord(hub, "smart_devices", map[string]any{
"system": system.Id,
"name": "/dev/sdb",
"state": "PASSED",
})
assert.NoError(t, err)
// Re-fetch the record so PocketBase can properly track original values
smartDevice, err = hub.FindRecordById("smart_devices", smartDevice.Id)
assert.NoError(t, err)
// Update the smart device state to FAILED
smartDevice.Set("state", "FAILED")
err = hub.Save(smartDevice)
assert.NoError(t, err)
time.Sleep(50 * time.Millisecond)
// Verify that an email was sent
assert.EqualValues(t, 1, hub.TestMailer.TotalSend(), "should have 1 email sent")
// Check that the email doesn't have empty parentheses for missing model
lastMessage := hub.TestMailer.LastMessage()
assert.NotContains(t, lastMessage.Text, "()", "should not have empty parentheses for missing model")
assert.Contains(t, lastMessage.Text, "/dev/sdb")
}

View File

@@ -166,6 +166,7 @@ func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, a
return am.SendAlert(AlertMessageData{
UserID: alertRecord.GetString("user"),
SystemID: systemID,
Title: title,
Message: message,
Link: am.hub.MakeLink("system", systemID),

View File

@@ -66,17 +66,30 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
unit = ""
case "GPU":
val = data.Info.GpuPct
case "Battery":
if data.Stats.Battery[0] == 0 {
continue
}
val = float64(data.Stats.Battery[0])
}
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
// Battery alert has inverted logic: trigger when value is BELOW threshold
lowAlert := isLowAlert(name)
// CONTINUE
// IF alert is not triggered and curValue is less than threshold
// OR alert is triggered and curValue is greater than threshold
if (!triggered && val <= threshold) || (triggered && val > threshold) {
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
continue
// For normal alerts: IF not triggered and curValue <= threshold, OR triggered and curValue > threshold
// For low alerts (Battery): IF not triggered and curValue >= threshold, OR triggered and curValue < threshold
if lowAlert {
if (!triggered && val >= threshold) || (triggered && val < threshold) {
continue
}
} else {
if (!triggered && val <= threshold) || (triggered && val > threshold) {
continue
}
}
min := max(1, cast.ToUint8(alertRecord.Get("min")))
@@ -94,7 +107,11 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
// send alert immediately if min is 1 - no need to sum up values.
if min == 1 {
alert.triggered = val > threshold
if lowAlert {
alert.triggered = val < threshold
} else {
alert.triggered = val > threshold
}
go am.sendSystemAlert(alert)
continue
}
@@ -219,6 +236,8 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
}
}
alert.val += maxUsage
case "Battery":
alert.val += float64(stats.Battery[0])
default:
continue
}
@@ -256,12 +275,24 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
// pass through alert if count is greater than or equal to minCount
if float32(alert.count) >= minCount {
if !alert.triggered && alert.val > alert.threshold {
alert.triggered = true
go am.sendSystemAlert(alert)
} else if alert.triggered && alert.val <= alert.threshold {
alert.triggered = false
go am.sendSystemAlert(alert)
// Battery alert has inverted logic: trigger when value is BELOW threshold
lowAlert := isLowAlert(alert.name)
if lowAlert {
if !alert.triggered && alert.val < alert.threshold {
alert.triggered = true
go am.sendSystemAlert(alert)
} else if alert.triggered && alert.val >= alert.threshold {
alert.triggered = false
go am.sendSystemAlert(alert)
}
} else {
if !alert.triggered && alert.val > alert.threshold {
alert.triggered = true
go am.sendSystemAlert(alert)
} else if alert.triggered && alert.val <= alert.threshold {
alert.triggered = false
go am.sendSystemAlert(alert)
}
}
}
}
@@ -288,10 +319,19 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
}
var subject string
lowAlert := isLowAlert(alert.name)
if alert.triggered {
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
if lowAlert {
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
} else {
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
}
} else {
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
if lowAlert {
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
} else {
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
}
}
minutesLabel := "minute"
if alert.min > 1 {
@@ -309,9 +349,14 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
}
am.SendAlert(AlertMessageData{
UserID: alert.alertRecord.GetString("user"),
SystemID: alert.systemRecord.Id,
Title: subject,
Message: body,
Link: am.hub.MakeLink("system", alert.systemRecord.Id),
LinkText: "View " + systemName,
})
}
func isLowAlert(name string) bool {
return name == "Battery"
}

View File

@@ -17,9 +17,8 @@ import (
type cmdOptions struct {
key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on.
// TODO: add hubURL and token
// hubURL string // hubURL is the URL of the hub to use.
// token string // token is the token to use for authentication.
hubURL string // hubURL is the URL of the Beszel hub.
token string // token is the token to use for authentication.
}
// parse parses the command line flags and populates the config struct.
@@ -47,13 +46,13 @@ func (opts *cmdOptions) parse() bool {
// 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")
pflag.StringVarP(&opts.hubURL, "url", "u", "", "URL of the Beszel hub")
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"}
flagsToConvert := []string{"key", "listen", "url", "token"}
for i, arg := range os.Args {
for _, flag := range flagsToConvert {
singleDash := "-" + flag
@@ -95,6 +94,13 @@ func (opts *cmdOptions) parse() bool {
return true
}
// Set environment variables from CLI flags (if provided)
if opts.hubURL != "" {
os.Setenv("HUB_URL", opts.hubURL)
}
if opts.token != "" {
os.Setenv("TOKEN", opts.token)
}
return false
}

View File

@@ -58,8 +58,8 @@ type FingerprintResponse struct {
}
type DataRequestOptions struct {
CacheTimeMs uint16 `cbor:"0,keyasint"`
// ResourceType uint8 `cbor:"1,keyasint,omitempty,omitzero"`
CacheTimeMs uint16 `cbor:"0,keyasint"`
IncludeDetails bool `cbor:"1,keyasint"`
}
type ContainerLogsRequest struct {

View File

@@ -17,7 +17,7 @@ RUN rm -rf /tmp/*
# --------------------------
# Final image: default scratch-based agent
# --------------------------
FROM alpine:latest
FROM alpine:3.22
COPY --from=builder /agent /agent
RUN apk add --no-cache smartmontools

View File

@@ -16,7 +16,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
# Final image
# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume
# --------------------------
FROM alpine:edge
FROM alpine:3.22
COPY --from=builder /agent /agent

View File

@@ -34,6 +34,14 @@ type ApiStats struct {
MemoryStats MemoryStats `json:"memory_stats"`
}
// Docker system info from /info API endpoint
type HostInfo struct {
OperatingSystem string `json:"OperatingSystem"`
KernelVersion string `json:"KernelVersion"`
NCPU int `json:"NCPU"`
MemTotal uint64 `json:"MemTotal"`
}
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem

View File

@@ -123,30 +123,49 @@ const (
ConnectionTypeWebSocket
)
// Core system data that is needed in All Systems table
type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Cores int `json:"c" cbor:"2,keyasint"`
Hostname string `json:"h,omitempty" cbor:"0,keyasint,omitempty"` // deprecated - moved to Details struct
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
// Threads is needed in Info struct to calculate load average thresholds
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
CpuModel string `json:"m" cbor:"4,keyasint"`
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
Uptime uint64 `json:"u" cbor:"5,keyasint"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
MemPct float64 `json:"mp" cbor:"7,keyasint"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
AgentVersion string `json:"v" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
// TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
Services []uint16 `json:"sv,omitempty" cbor:"22,keyasint,omitempty"` // [totalServices, numFailedServices]
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
}
// Data that does not change during process lifetime and is not needed in All Systems table
type Details struct {
Hostname string `cbor:"0,keyasint"`
Kernel string `cbor:"1,keyasint,omitempty"`
Cores int `cbor:"2,keyasint"`
Threads int `cbor:"3,keyasint"`
CpuModel string `cbor:"4,keyasint"`
Os Os `cbor:"5,keyasint"`
OsName string `cbor:"6,keyasint"`
Arch string `cbor:"7,keyasint"`
Podman bool `cbor:"8,keyasint,omitempty"`
MemoryTotal uint64 `cbor:"9,keyasint"`
SmartInterval time.Duration `cbor:"10,keyasint,omitempty"`
}
// Final data structure to return to the hub
@@ -155,4 +174,5 @@ type CombinedData struct {
Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
Details *Details `cbor:"4,keyasint,omitempty"`
}

View File

@@ -415,7 +415,11 @@ func TestExpiryMap_RemoveValue_WithExpiration(t *testing.T) {
// Wait for first value to expire
time.Sleep(time.Millisecond * 20)
// Try to remove the expired value - should remove one of the "value1" entries
// Trigger lazy cleanup of the expired key
_, ok := em.GetOk("key1")
assert.False(t, ok)
// Try to remove the remaining "value1" entry (key3)
removedValue, ok := em.RemovebyValue("value1")
assert.True(t, ok)
assert.Equal(t, "value1", removedValue)
@@ -423,14 +427,9 @@ func TestExpiryMap_RemoveValue_WithExpiration(t *testing.T) {
// Should still have key2 (different value)
assert.True(t, em.Has("key2"))
// Should have removed one of the "value1" entries (either key1 or key3)
// But we can't predict which one due to map iteration order
key1Exists := em.Has("key1")
key3Exists := em.Has("key3")
// Exactly one of key1 or key3 should be gone
assert.False(t, key1Exists && key3Exists) // Both shouldn't exist
assert.True(t, key1Exists || key3Exists) // At least one should still exist
// key1 should be gone due to expiration and key3 should be removed by value.
assert.False(t, em.Has("key1"))
assert.False(t, em.Has("key3"))
}
func TestExpiryMap_ValueOperations_Integration(t *testing.T) {

View File

@@ -268,8 +268,8 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
// update / delete user alerts
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
// get SMART data
apiAuth.GET("/smart", h.getSmartData)
// refresh SMART devices for a system
apiAuth.POST("/smart/refresh", h.refreshSmartData)
// get systemd service details
apiAuth.GET("/systemd/info", h.getSystemdInfo)
// /containers routes
@@ -365,22 +365,25 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]any{"details": details})
}
// getSmartData handles GET /api/beszel/smart requests
func (h *Hub) getSmartData(e *core.RequestEvent) error {
// refreshSmartData handles POST /api/beszel/smart/refresh requests
// Fetches fresh SMART data from the agent and updates the collection
func (h *Hub) refreshSmartData(e *core.RequestEvent) error {
systemID := e.Request.URL.Query().Get("system")
if systemID == "" {
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system parameter is required"})
}
system, err := h.sm.GetSystem(systemID)
if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": "system not found"})
}
data, err := system.FetchSmartDataFromAgent()
if err != nil {
return e.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
// Fetch and save SMART devices
if err := system.FetchAndSaveSmartDevices(); err != nil {
return e.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
e.Response.Header().Set("Cache-Control", "public, max-age=60")
return e.JSON(http.StatusOK, data)
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
}
// generates key pair if it doesn't exist and returns signer

View File

@@ -9,6 +9,7 @@ import (
"math/rand"
"net"
"strings"
"sync/atomic"
"time"
"github.com/henrygd/beszel/internal/common"
@@ -28,18 +29,22 @@ import (
)
type System struct {
Id string `db:"id"`
Host string `db:"host"`
Port string `db:"port"`
Status string `db:"status"`
manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent
ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection
agentVersion semver.Version // Agent version
updateTicker *time.Ticker // Ticker for updating the system
Id string `db:"id"`
Host string `db:"host"`
Port string `db:"port"`
Status string `db:"status"`
manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent
ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection
agentVersion semver.Version // Agent version
updateTicker *time.Ticker // Ticker for updating the system
detailsFetched atomic.Bool // True if static system details have been fetched and saved
smartFetching atomic.Bool // True if SMART devices are currently being fetched
smartInterval time.Duration // Interval for periodic SMART data updates
lastSmartFetch atomic.Int64 // Unix milliseconds of last SMART data fetch
}
func (sm *SystemManager) NewSystem(systemId string) *System {
@@ -112,10 +117,37 @@ func (sys *System) update() error {
sys.handlePaused()
return nil
}
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: uint16(interval)})
if err == nil {
_, err = sys.createRecords(data)
options := common.DataRequestOptions{
CacheTimeMs: uint16(interval),
}
// fetch system details if not already fetched
if !sys.detailsFetched.Load() {
options.IncludeDetails = true
}
data, err := sys.fetchDataFromAgent(options)
if err != nil {
return err
}
// create system records
_, err = sys.createRecords(data)
// Fetch and save SMART devices when system first comes online or at intervals
if backgroundSmartFetchEnabled() {
if sys.smartInterval <= 0 {
sys.smartInterval = time.Hour
}
lastFetch := sys.lastSmartFetch.Load()
if time.Since(time.UnixMilli(lastFetch)) >= sys.smartInterval && sys.smartFetching.CompareAndSwap(false, true) {
go func() {
defer sys.smartFetching.Store(false)
sys.lastSmartFetch.Store(time.Now().UnixMilli())
_ = sys.FetchAndSaveSmartDevices()
}()
}
}
return err
}
@@ -140,12 +172,11 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
hub := sys.manager.hub
err = hub.RunInTransaction(func(txApp core.App) error {
// add system_stats and container_stats records
// add system_stats record
systemStatsCollection, err := txApp.FindCachedCollectionByNameOrId("system_stats")
if err != nil {
return err
}
systemStatsRecord := core.NewRecord(systemStatsCollection)
systemStatsRecord.Set("system", systemRecord.Id)
systemStatsRecord.Set("stats", data.Stats)
@@ -153,14 +184,14 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
if err := txApp.SaveNoValidate(systemStatsRecord); err != nil {
return err
}
// add containers and container_stats records
if len(data.Containers) > 0 {
// add / update containers records
if data.Containers[0].Id != "" {
if err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {
return err
}
}
// add new container_stats record
containerStatsCollection, err := txApp.FindCachedCollectionByNameOrId("container_stats")
if err != nil {
return err
@@ -181,9 +212,20 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
}
// add system details record
if data.Details != nil {
if err := createSystemDetailsRecord(txApp, data.Details, sys.Id); err != nil {
return err
}
sys.detailsFetched.Store(true)
// update smart interval if it's set on the agent side
if data.Details.SmartInterval > 0 {
sys.smartInterval = data.Details.SmartInterval
}
}
// update system record (do this last because it triggers alerts and we need above records to be inserted first)
systemRecord.Set("status", up)
systemRecord.Set("info", data.Info)
if err := txApp.SaveNoValidate(systemRecord); err != nil {
return err
@@ -194,6 +236,31 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
return systemRecord, err
}
func createSystemDetailsRecord(app core.App, data *system.Details, systemId string) error {
collectionName := "system_details"
params := dbx.Params{
"id": systemId,
"system": systemId,
"hostname": data.Hostname,
"kernel": data.Kernel,
"cores": data.Cores,
"threads": data.Threads,
"cpu": data.CpuModel,
"os": data.Os,
"os_name": data.OsName,
"arch": data.Arch,
"memory": data.MemoryTotal,
"podman": data.Podman,
"updated": time.Now().UTC(),
}
result, err := app.DB().Update(collectionName, params, dbx.HashExp{"id": systemId}).Execute()
rowsAffected, _ := result.RowsAffected()
if err != nil || rowsAffected == 0 {
_, err = app.DB().Insert(collectionName, params).Execute()
}
return err
}
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
if len(data) == 0 {
return nil
@@ -208,7 +275,7 @@ func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId s
for i, service := range data {
suffix := fmt.Sprintf("%d", i)
valueStrings = append(valueStrings, fmt.Sprintf("({:id%[1]s}, {:system}, {:name%[1]s}, {:state%[1]s}, {:sub%[1]s}, {:cpu%[1]s}, {:cpuPeak%[1]s}, {:memory%[1]s}, {:memPeak%[1]s}, {:updated})", suffix))
params["id"+suffix] = getSystemdServiceId(systemId, service.Name)
params["id"+suffix] = makeStableHashId(systemId, service.Name)
params["name"+suffix] = service.Name
params["state"+suffix] = service.State
params["sub"+suffix] = service.Sub
@@ -225,13 +292,6 @@ func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId s
return err
}
// getSystemdServiceId generates a deterministic unique id for a systemd service
func getSystemdServiceId(systemId string, serviceName string) string {
hash := fnv.New32a()
hash.Write([]byte(systemId + serviceName))
return fmt.Sprintf("%x", hash.Sum32())
}
// createContainerRecords creates container records
func createContainerRecords(app core.App, data []*container.Stats, systemId string) error {
if len(data) == 0 {
@@ -435,43 +495,12 @@ func (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.Servic
return result, err
}
// FetchSmartDataFromAgent fetches SMART data from the agent
func (sys *System) FetchSmartDataFromAgent() (map[string]any, error) {
// fetch via websocket
if sys.WsConn != nil && sys.WsConn.IsConnected() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return sys.WsConn.RequestSmartData(ctx)
func makeStableHashId(strings ...string) string {
hash := fnv.New32a()
for _, str := range strings {
hash.Write([]byte(str))
}
// fetch via SSH
var result map[string]any
err := sys.runSSHOperation(5*time.Second, 1, func(session *ssh.Session) (bool, error) {
stdout, err := session.StdoutPipe()
if err != nil {
return false, err
}
stdin, stdinErr := session.StdinPipe()
if stdinErr != nil {
return false, stdinErr
}
if err := session.Shell(); err != nil {
return false, err
}
req := common.HubRequest[any]{Action: common.GetSmartData}
_ = cbor.NewEncoder(stdin).Encode(req)
_ = stdin.Close()
var resp common.AgentResponse
if err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {
return false, err
}
// Convert to generic map for JSON response
result = make(map[string]any, len(resp.SmartData))
for k, v := range resp.SmartData {
result[k] = v
}
return false, nil
})
return result, err
return fmt.Sprintf("%x", hash.Sum32())
}
// fetchDataViaSSH handles fetching data using SSH.

View File

@@ -0,0 +1,132 @@
package systems
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"github.com/fxamacker/cbor/v2"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/smart"
"github.com/pocketbase/pocketbase/core"
"golang.org/x/crypto/ssh"
)
// FetchSmartDataFromAgent fetches SMART data from the agent
func (sys *System) FetchSmartDataFromAgent() (map[string]smart.SmartData, error) {
// fetch via websocket
if sys.WsConn != nil && sys.WsConn.IsConnected() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return sys.WsConn.RequestSmartData(ctx)
}
// fetch via SSH
var result map[string]smart.SmartData
err := sys.runSSHOperation(5*time.Second, 1, func(session *ssh.Session) (bool, error) {
stdout, err := session.StdoutPipe()
if err != nil {
return false, err
}
stdin, stdinErr := session.StdinPipe()
if stdinErr != nil {
return false, stdinErr
}
if err := session.Shell(); err != nil {
return false, err
}
req := common.HubRequest[any]{Action: common.GetSmartData}
_ = cbor.NewEncoder(stdin).Encode(req)
_ = stdin.Close()
var resp common.AgentResponse
if err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {
return false, err
}
result = resp.SmartData
return false, nil
})
return result, err
}
// FetchAndSaveSmartDevices fetches SMART data from the agent and saves it to the database
func (sys *System) FetchAndSaveSmartDevices() error {
smartData, err := sys.FetchSmartDataFromAgent()
if err != nil || len(smartData) == 0 {
return err
}
return sys.saveSmartDevices(smartData)
}
// saveSmartDevices saves SMART device data to the smart_devices collection
func (sys *System) saveSmartDevices(smartData map[string]smart.SmartData) error {
if len(smartData) == 0 {
return nil
}
hub := sys.manager.hub
collection, err := hub.FindCachedCollectionByNameOrId("smart_devices")
if err != nil {
return err
}
for deviceKey, device := range smartData {
if err := sys.upsertSmartDeviceRecord(collection, deviceKey, device); err != nil {
return err
}
}
return nil
}
func (sys *System) upsertSmartDeviceRecord(collection *core.Collection, deviceKey string, device smart.SmartData) error {
hub := sys.manager.hub
recordID := makeStableHashId(sys.Id, deviceKey)
record, err := hub.FindRecordById(collection, recordID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return err
}
record = core.NewRecord(collection)
record.Set("id", recordID)
}
name := device.DiskName
if name == "" {
name = deviceKey
}
powerOnHours, powerCycles := extractPowerMetrics(device.Attributes)
record.Set("system", sys.Id)
record.Set("name", name)
record.Set("model", device.ModelName)
record.Set("state", device.SmartStatus)
record.Set("capacity", device.Capacity)
record.Set("temp", device.Temperature)
record.Set("firmware", device.FirmwareVersion)
record.Set("serial", device.SerialNumber)
record.Set("type", device.DiskType)
record.Set("hours", powerOnHours)
record.Set("cycles", powerCycles)
record.Set("attributes", device.Attributes)
return hub.SaveNoValidate(record)
}
// extractPowerMetrics extracts power on hours and power cycles from SMART attributes
func extractPowerMetrics(attributes []*smart.SmartAttribute) (powerOnHours, powerCycles uint64) {
for _, attr := range attributes {
nameLower := strings.ToLower(attr.Name)
if powerOnHours == 0 && (strings.Contains(nameLower, "poweronhours") || strings.Contains(nameLower, "power_on_hours")) {
powerOnHours = attr.RawValue
}
if powerCycles == 0 && ((strings.Contains(nameLower, "power") && strings.Contains(nameLower, "cycle")) || strings.Contains(nameLower, "startstopcycles")) {
powerCycles = attr.RawValue
}
if powerOnHours > 0 && powerCycles > 0 {
break
}
}
return
}

View File

@@ -14,9 +14,9 @@ func TestGetSystemdServiceId(t *testing.T) {
serviceName := "nginx.service"
// Call multiple times and ensure same result
id1 := getSystemdServiceId(systemId, serviceName)
id2 := getSystemdServiceId(systemId, serviceName)
id3 := getSystemdServiceId(systemId, serviceName)
id1 := makeStableHashId(systemId, serviceName)
id2 := makeStableHashId(systemId, serviceName)
id3 := makeStableHashId(systemId, serviceName)
assert.Equal(t, id1, id2)
assert.Equal(t, id2, id3)
@@ -29,10 +29,10 @@ func TestGetSystemdServiceId(t *testing.T) {
serviceName1 := "nginx.service"
serviceName2 := "apache.service"
id1 := getSystemdServiceId(systemId1, serviceName1)
id2 := getSystemdServiceId(systemId2, serviceName1)
id3 := getSystemdServiceId(systemId1, serviceName2)
id4 := getSystemdServiceId(systemId2, serviceName2)
id1 := makeStableHashId(systemId1, serviceName1)
id2 := makeStableHashId(systemId2, serviceName1)
id3 := makeStableHashId(systemId1, serviceName2)
id4 := makeStableHashId(systemId2, serviceName2)
// All IDs should be different
assert.NotEqual(t, id1, id2)
@@ -56,14 +56,14 @@ func TestGetSystemdServiceId(t *testing.T) {
}
for _, tc := range testCases {
id := getSystemdServiceId(tc.systemId, tc.serviceName)
id := makeStableHashId(tc.systemId, tc.serviceName)
// FNV-32 produces 8 hex characters
assert.Len(t, id, 8, "ID should be 8 characters for systemId='%s', serviceName='%s'", tc.systemId, tc.serviceName)
}
})
t.Run("hexadecimal output", func(t *testing.T) {
id := getSystemdServiceId("test-system", "test-service")
id := makeStableHashId("test-system", "test-service")
assert.NotEmpty(t, id)
// Should only contain hexadecimal characters

View File

@@ -0,0 +1,10 @@
//go:build !testing
// +build !testing
package systems
// Background SMART fetching is enabled in production but disabled for tests (systems_test_helpers.go).
//
// The hub integration tests create/replace systems and clean up the test apps quickly.
// Background SMART fetching can outlive teardown and crash in PocketBase internals (nil DB).
func backgroundSmartFetchEnabled() bool { return true }

View File

@@ -266,18 +266,20 @@ func testOld(t *testing.T, hub *tests.TestHub) {
// Create test system data
testData := &system.CombinedData{
Details: &system.Details{
Hostname: "data-test.example.com",
Kernel: "5.15.0-generic",
Cores: 4,
Threads: 8,
CpuModel: "Test CPU",
},
Info: system.Info{
Hostname: "data-test.example.com",
KernelVersion: "5.15.0-generic",
Cores: 4,
Threads: 8,
CpuModel: "Test CPU",
Uptime: 3600,
Cpu: 25.5,
MemPct: 40.2,
DiskPct: 60.0,
Bandwidth: 100.0,
AgentVersion: "1.0.0",
Uptime: 3600,
Cpu: 25.5,
MemPct: 40.2,
DiskPct: 60.0,
Bandwidth: 100.0,
AgentVersion: "1.0.0",
},
Stats: system.Stats{
Cpu: 25.5,

View File

@@ -10,6 +10,13 @@ import (
entities "github.com/henrygd/beszel/internal/entities/system"
)
// The hub integration tests create/replace systems and cleanup the test apps quickly.
// Background SMART fetching can outlive teardown and crash in PocketBase internals (nil DB).
//
// We keep the explicit SMART refresh endpoint / method available, but disable
// the automatic background fetch during tests.
func backgroundSmartFetchEnabled() bool { return false }
// TESTING ONLY: GetSystemCount returns the number of systems in the store
func (sm *SystemManager) GetSystemCount() int {
return sm.systems.Length()

View File

@@ -6,6 +6,7 @@ import (
"github.com/fxamacker/cbor/v2"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/smart"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/henrygd/beszel/internal/entities/systemd"
"github.com/lxzan/gws"
@@ -155,7 +156,7 @@ func (h *systemdInfoHandler) Handle(agentResponse common.AgentResponse) error {
////////////////////////////////////////////////////////////////////////////
// RequestSmartData requests SMART data via WebSocket.
func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]any, error) {
func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]smart.SmartData, error) {
if !ws.IsConnected() {
return nil, gws.ErrConnClosed
}
@@ -163,7 +164,7 @@ func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]any, error)
if err != nil {
return nil, err
}
var result map[string]any
var result map[string]smart.SmartData
handler := ResponseHandler(&smartDataHandler{result: &result})
if err := ws.handleAgentRequest(req, handler); err != nil {
return nil, err
@@ -174,19 +175,14 @@ func (ws *WsConn) RequestSmartData(ctx context.Context) (map[string]any, error)
// smartDataHandler parses SMART data map from AgentResponse
type smartDataHandler struct {
BaseHandler
result *map[string]any
result *map[string]smart.SmartData
}
func (h *smartDataHandler) Handle(agentResponse common.AgentResponse) error {
if agentResponse.SmartData == nil {
return errors.New("no SMART data in response")
}
// convert to map[string]any for transport convenience in hub layer
out := make(map[string]any, len(agentResponse.SmartData))
for k, v := range agentResponse.SmartData {
out[k] = v
}
*h.result = out
*h.result = agentResponse.SmartData
return nil
}

View File

@@ -78,7 +78,8 @@ func init() {
"GPU",
"LoadAvg1",
"LoadAvg5",
"LoadAvg15"
"LoadAvg15",
"Battery"
]
},
{
@@ -1150,6 +1151,472 @@ func init() {
"type": "base",
"updateRule": null,
"viewRule": null
},
{
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{10}",
"hidden": false,
"id": "text3208210256",
"max": 10,
"min": 10,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "_pb_users_auth_",
"hidden": false,
"id": "relation2375276105",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"cascadeDelete": true,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "relation3377271179",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "select2844932856",
"maxSelect": 1,
"name": "type",
"presentable": false,
"required": true,
"system": false,
"type": "select",
"values": [
"one-time",
"daily"
]
},
{
"hidden": false,
"id": "date2675529103",
"max": "",
"min": "",
"name": "start",
"presentable": false,
"required": true,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "date16528305",
"max": "",
"min": "",
"name": "end",
"presentable": false,
"required": true,
"system": false,
"type": "date"
}
],
"id": "pbc_451525641",
"indexes": [
"CREATE INDEX ` + "`" + `idx_q0iKnRP9v8` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `system` + "`" + `\n)",
"CREATE INDEX ` + "`" + `idx_6T7ljT7FJd` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `type` + "`" + `,\n ` + "`" + `end` + "`" + `\n)"
],
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"name": "quiet_hours",
"system": false,
"type": "base",
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
"viewRule": "@request.auth.id != \"\" && user.id = @request.auth.id"
},
{
"createRule": null,
"deleteRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{10}",
"hidden": false,
"id": "text3208210256",
"max": 10,
"min": 10,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "relation3377271179",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3616895705",
"max": 0,
"min": 0,
"name": "model",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2744374011",
"max": 0,
"min": 0,
"name": "state",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number3051925876",
"max": null,
"min": null,
"name": "capacity",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number190023114",
"max": null,
"min": null,
"name": "temp",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3589068740",
"max": 0,
"min": 0,
"name": "firmware",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3547646428",
"max": 0,
"min": 0,
"name": "serial",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2363381545",
"max": 0,
"min": 0,
"name": "type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number1234567890",
"max": null,
"min": null,
"name": "hours",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number0987654321",
"max": null,
"min": null,
"name": "cycles",
"onlyInt": true,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "json832282224",
"maxSize": 0,
"name": "attributes",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_2571630677",
"indexes": [
"CREATE INDEX ` + "`" + `idx_DZ9yhvgl44` + "`" + ` ON ` + "`" + `smart_devices` + "`" + ` (` + "`" + `system` + "`" + `)"
],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "smart_devices",
"system": false,
"type": "base",
"updateRule": null,
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
},
{
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": true,
"collectionId": "2hz5ncl8tizk5nx",
"hidden": false,
"id": "relation3377271179",
"maxSelect": 1,
"minSelect": 0,
"name": "system",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3847340049",
"max": 0,
"min": 0,
"name": "hostname",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number1789936913",
"max": null,
"min": null,
"name": "os",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2818598173",
"max": 0,
"min": 0,
"name": "os_name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1574083243",
"max": 0,
"min": 0,
"name": "kernel",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3128971310",
"max": 0,
"min": 0,
"name": "cpu",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text4161937994",
"max": 0,
"min": 0,
"name": "arch",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number4245036687",
"max": null,
"min": null,
"name": "cores",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1871592925",
"max": null,
"min": null,
"name": "threads",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number3933025333",
"max": null,
"min": null,
"name": "memory",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "bool2200265312",
"name": "podman",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_3116237454",
"indexes": [],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "system_details",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id"
}
]`

View File

@@ -498,6 +498,10 @@ func (rm *RecordManager) DeleteOldRecords() {
if err != nil {
return err
}
err = deleteOldQuietHours(txApp)
if err != nil {
return err
}
return nil
})
}
@@ -591,6 +595,17 @@ func deleteOldContainerRecords(app core.App) error {
return nil
}
// Deletes old quiet hours records where end date has passed
func deleteOldQuietHours(app core.App) error {
now := time.Now().UTC()
_, err := app.DB().NewQuery("DELETE FROM quiet_hours WHERE type = 'one-time' AND end < {:now}").Bind(dbx.Params{"now": now}).Execute()
if err != nil {
return err
}
return nil
}
/* Round float to two decimals */
func twoDecimals(value float64) float64 {
return math.Round(value*100) / 100

View File

@@ -17,6 +17,9 @@
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useButtonType": "off"
},
"complexity": {
"noUselessStringConcat": "error",
"noUselessUndefinedInitialization": "error",
@@ -30,13 +33,14 @@
"noUnusedFunctionParameters": "error",
"noUnusedPrivateClassMembers": "error",
"useExhaustiveDependencies": {
"level": "error",
"options": {
"reportUnnecessaryDependencies": false
}
"level": "off"
},
"useUniqueElementIds": "off",
"noUnusedVariables": "error"
},
"security": {
"noDangerouslySetInnerHtml": "warn"
},
"style": {
"noParameterProperties": "error",
"noYodaExpression": "error",
@@ -47,7 +51,8 @@
},
"suspicious": {
"useAwait": "error",
"noEvolvingTypes": "error"
"noEvolvingTypes": "error",
"noArrayIndexKey": "off"
}
}
},

996
internal/site/bun.lock Normal file
View File

@@ -0,0 +1,996 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "beszel",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
"@lingui/detect-locale": "^5.4.1",
"@lingui/macro": "^5.4.1",
"@lingui/react": "^5.4.1",
"@nanostores/react": "^0.7.3",
"@nanostores/router": "^0.11.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-direction": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"d3-time": "^3.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.452.0",
"nanostores": "^0.11.4",
"pocketbase": "^0.26.2",
"react": "^19.1.2",
"react-dom": "^19.1.2",
"recharts": "^2.15.4",
"shiki": "^3.13.0",
"tailwind-merge": "^3.3.1",
"valibot": "^0.42.1",
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@lingui/cli": "^5.4.1",
"@lingui/swc-plugin": "^5.6.1",
"@lingui/vite-plugin": "^5.4.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/vite": "^4.1.12",
"@types/bun": "^1.2.20",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react-swc": "^4.0.1",
"tailwindcss": "^4.1.12",
"tw-animate-css": "^1.3.7",
"typescript": "^5.9.2",
"vite": "^7.1.3",
},
"optionalDependencies": {
"@esbuild/linux-arm64": "^0.21.5",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
"@babel/core": ["@babel/core@7.28.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.3", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ=="],
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw=="],
"@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.6", "", { "os": "android", "cpu": "arm64" }, "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.6", "", { "os": "android", "cpu": "x64" }, "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.6", "", { "os": "linux", "cpu": "ia32" }, "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.6", "", { "os": "linux", "cpu": "x64" }, "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.6", "", { "os": "none", "cpu": "x64" }, "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.6", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.6", "", { "os": "openbsd", "cpu": "x64" }, "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.6", "", { "os": "sunos", "cpu": "x64" }, "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@henrygd/queue": ["@henrygd/queue@1.0.7", "", {}, "sha512-Jmt/iO6yDlz9UYGILkm/Qzi/ckkEiTNZcqDvt3QFLE4OThPeiCj6tKsynHFm/ppl8RumWXAx1dZPBPiRPaaGig=="],
"@henrygd/semaphore": ["@henrygd/semaphore@0.0.2", "", {}, "sha512-N3W7MKwTRmAxOjeG0NAT18oe2Xn3KdjkpMR6crbkF1UDamMGPjyigqEsefiv+qTaxibtc1a+zXCVzb9YXANVqw=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@lingui/babel-plugin-extract-messages": ["@lingui/babel-plugin-extract-messages@5.4.1", "", {}, "sha512-sjkVaLyuK3ZW62mv5gU6pOdl3ZpwDReeSaNodJuf9LssbMIQPa5WOirTnMeBaalrQ8BA2srrRzQAWgsXPQVdXA=="],
"@lingui/babel-plugin-lingui-macro": ["@lingui/babel-plugin-lingui-macro@5.4.1", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/runtime": "^7.20.13", "@babel/types": "^7.20.7", "@lingui/conf": "5.4.1", "@lingui/core": "5.4.1", "@lingui/message-utils": "5.4.1" }, "peerDependencies": { "babel-plugin-macros": "2 || 3" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9IO+PDvdneY8OCI8zvI1oDXpzryTMtyRv7uq9O0U1mFCvIPVd5dWQKQDu/CpgpYAc2+JG/izn5PNl9xzPc6ckw=="],
"@lingui/cli": ["@lingui/cli@5.4.1", "", { "dependencies": { "@babel/core": "^7.21.0", "@babel/generator": "^7.21.1", "@babel/parser": "^7.22.0", "@babel/runtime": "^7.21.0", "@babel/types": "^7.21.2", "@lingui/babel-plugin-extract-messages": "5.4.1", "@lingui/babel-plugin-lingui-macro": "5.4.1", "@lingui/conf": "5.4.1", "@lingui/core": "5.4.1", "@lingui/format-po": "5.4.1", "@lingui/message-utils": "5.4.1", "chokidar": "3.5.1", "cli-table": "^0.3.11", "commander": "^10.0.0", "convert-source-map": "^2.0.0", "date-fns": "^3.6.0", "esbuild": "^0.25.1", "glob": "^11.0.0", "micromatch": "^4.0.7", "normalize-path": "^3.0.0", "ora": "^5.1.0", "picocolors": "^1.1.1", "pofile": "^1.1.4", "pseudolocale": "^2.0.0", "source-map": "^0.8.0-beta.0" }, "bin": { "lingui": "dist/lingui.js" } }, "sha512-UAKA9Iz4zMDJS7fzWMZ4hzQWontrTBnI5XCsPm7ttB0Ed0F4Pwph/Vu7pg4bJdiYr4d6nqEpRWd9aTxcC15/IA=="],
"@lingui/conf": ["@lingui/conf@5.4.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "cosmiconfig": "^8.0.0", "jest-validate": "^29.4.3", "jiti": "^1.17.1", "picocolors": "^1.1.1" } }, "sha512-aDkj/bMSr/mCL8Nr1TS52v0GLCuVa4YqtRz+WvUCFZw/ovVInX0hKq1TClx/bSlhu60FzB/CbclxFMBw8aLVUg=="],
"@lingui/core": ["@lingui/core@5.4.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@lingui/message-utils": "5.4.1" }, "peerDependencies": { "@lingui/babel-plugin-lingui-macro": "5.4.1", "babel-plugin-macros": "2 || 3" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-4FeIh56PH5vziPg2BYo4XYWWOHE4XaY/XR8Jakwn0/qwtLpydWMNVpZOpGWi7nfPZtcLaJLmZKup6UNxEl1Pfw=="],
"@lingui/detect-locale": ["@lingui/detect-locale@5.4.1", "", {}, "sha512-X6qM6Uw6EhcQj3z5sOIR/wVmhhJfa3SCvMLOzFxCWvm90yocMvuFrCZnmlhvzcGy6xLYO3PA/smHnAmWKUlU3g=="],
"@lingui/format-po": ["@lingui/format-po@5.4.1", "", { "dependencies": { "@lingui/conf": "5.4.1", "@lingui/message-utils": "5.4.1", "date-fns": "^3.6.0", "pofile": "^1.1.4" } }, "sha512-IBVq3RRLNEVRzNZcdEw0qpM5NKX4e9wDmvJMorkR2OYrgTbhWx5gDYhXpEZ9yqtuEVhILMdriVNjAAUnDAJibA=="],
"@lingui/macro": ["@lingui/macro@5.4.1", "", { "dependencies": { "@lingui/core": "5.4.1", "@lingui/react": "5.4.1" }, "peerDependencies": { "@lingui/babel-plugin-lingui-macro": "5.4.1", "babel-plugin-macros": "2 || 3" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-tBpcZCmyppe1OjMQyka+SvexG+iIWSlecmeMoZhf4bRWEDlGTfIuAoosZhVqsiyaaaBNJqpopOxJXf/Tgm7oqQ=="],
"@lingui/message-utils": ["@lingui/message-utils@5.4.1", "", { "dependencies": { "@messageformat/parser": "^5.0.0", "js-sha256": "^0.10.1" } }, "sha512-hXfL90fFBoKp5YgLaWo3HbJS/7q+WlWs7VwVbUxl4pa+YladqNZf08JoDeBUDtlEVx5a3bNUSACXHs2FZo12aw=="],
"@lingui/react": ["@lingui/react@5.4.1", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@lingui/core": "5.4.1" }, "peerDependencies": { "@lingui/babel-plugin-lingui-macro": "5.4.1", "babel-plugin-macros": "2 || 3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-BfWHRTyu+Je4Km9ZYCTgFuRHgQI9TJa/fMYKJBw3dNy3I09oIqnJ21qbdDimnb/Z9ztMCGQ75EIFoqAB6bdwaw=="],
"@lingui/swc-plugin": ["@lingui/swc-plugin@5.6.1", "", { "peerDependencies": { "@lingui/core": "5" } }, "sha512-kT/ghCKMlTa+SJZU/xn2vvU1QE3/NO3m3Feg6r2OVOovAB6VHKShVElU5truBC2KXn/cPqE9Kz2Yj0+jUmO6xQ=="],
"@lingui/vite-plugin": ["@lingui/vite-plugin@5.4.1", "", { "dependencies": { "@lingui/cli": "5.4.1", "@lingui/conf": "5.4.1" }, "peerDependencies": { "vite": "^3 || ^4 || ^5.0.9 || ^6 || ^7" } }, "sha512-4BxkHliJdGk7lmD++Bee9iy+n66kUONUPgpNqEgcuoEfaL0UgWWLbpkOr42X3tMUVt/S/SUM7firx6NexSCJ4Q=="],
"@messageformat/parser": ["@messageformat/parser@5.1.1", "", { "dependencies": { "moo": "^0.5.1" } }, "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg=="],
"@nanostores/react": ["@nanostores/react@0.7.3", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0", "react": ">=18.0.0" } }, "sha512-/XuLAMENRu/Q71biW4AZ4qmU070vkZgiQ28gaTSNRPm2SZF5zGAR81zPE1MaMB4SeOp6ZTst92NBaG75XSspNg=="],
"@nanostores/router": ["@nanostores/router@0.11.0", "", { "peerDependencies": { "nanostores": "^0.9.0" } }, "sha512-QlcneDqXVIsQL3agOS59d9gJQ/9M3qyVOWVttARL5Xvpmovtq91oOYcQxKbLq9i7iitGs5yDJmabe/O3QjWddQ=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.32", "", {}, "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.48.1", "", { "os": "android", "cpu": "arm" }, "sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.48.1", "", { "os": "android", "cpu": "arm64" }, "sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.48.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.48.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.48.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.48.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.48.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.48.1", "", { "os": "linux", "cpu": "arm" }, "sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.48.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.48.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.48.1", "", { "os": "linux", "cpu": "none" }, "sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.48.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.48.1", "", { "os": "linux", "cpu": "none" }, "sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.48.1", "", { "os": "linux", "cpu": "none" }, "sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.48.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.48.1", "", { "os": "linux", "cpu": "x64" }, "sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.48.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.48.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.48.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.48.1", "", { "os": "win32", "cpu": "x64" }, "sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg=="],
"@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
"@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],
"@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],
"@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="],
"@swc/core": ["@swc/core@1.13.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-x64": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-musl": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.5", "", { "os": "win32", "cpu": "x64" }, "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.24", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng=="],
"@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "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" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="],
"@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="],
"@types/d3-shape": ["@types/d3-shape@3.1.6", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA=="],
"@types/d3-time": ["@types/d3-time@3.0.3", "", {}, "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
"@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="],
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.32", "@swc/core": "^1.13.2" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-NQhPjysi5duItyrMd5JWZFf2vNOuSMyw+EoZyTBDzk+DkfYD8WNrsUs09sELV2cr1P15nufsN25hsUBt4CKF9Q=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": "cli.js" }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@3.5.1", "", { "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.5.0" }, "optionalDependencies": { "fsevents": "~2.3.1" } }, "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"cli-table": ["cli-table@0.3.11", "", { "dependencies": { "colors": "1.0.3" } }, "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ=="],
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colors": ["colors@1.0.3", "", {}, "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" } }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.182", "", {}, "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
"esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": "bin/esbuild" }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"foreground-child": ["foreground-child@3.2.1", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob": ["glob@11.0.1", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": "dist/esm/bin.mjs" }, "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@4.1.0", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw=="],
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
"jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="],
"jiti": ["jiti@1.21.7", "", { "bin": "bin/jiti.js" }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"js-sha256": ["js-sha256@0.10.1", "", {}, "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.452.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-kNefjOUOGm+Mu3KDiryONyPba9r+nhcrz5oJs3N6JDzGboQNEXw5GB3yB8rnV9/FA4bPyggNU6CRSihZm9MvSw=="],
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": "dist/cjs/src/bin.js" }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"moo": ["moo@0.5.2", "", {}, "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"package-json-from-dist": ["package-json-from-dist@1.0.0", "", {}, "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pocketbase": ["pocketbase@0.26.2", "", {}, "sha512-WA8EOBc3QnSJh8rJ3iYoi9DmmPOMFIgVfAmIGux7wwruUEIzXgvrO4u0W2htfQjGIcyezJkdZOy5Xmh7SxAftw=="],
"pofile": ["pofile@1.1.4", "", {}, "sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"pseudolocale": ["pseudolocale@2.1.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": "dist/cli.mjs" }, "sha512-af5fsrRvVwD+MBasBJvuDChT0KDqT0nEwD9NTgbtHJ16FKomWac9ua0z6YVNB4G9x9IOaiGWym62aby6n4tFMA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "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" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@3.5.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ=="],
"recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"rollup": ["rollup@4.48.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.48.1", "@rollup/rollup-android-arm64": "4.48.1", "@rollup/rollup-darwin-arm64": "4.48.1", "@rollup/rollup-darwin-x64": "4.48.1", "@rollup/rollup-freebsd-arm64": "4.48.1", "@rollup/rollup-freebsd-x64": "4.48.1", "@rollup/rollup-linux-arm-gnueabihf": "4.48.1", "@rollup/rollup-linux-arm-musleabihf": "4.48.1", "@rollup/rollup-linux-arm64-gnu": "4.48.1", "@rollup/rollup-linux-arm64-musl": "4.48.1", "@rollup/rollup-linux-loongarch64-gnu": "4.48.1", "@rollup/rollup-linux-ppc64-gnu": "4.48.1", "@rollup/rollup-linux-riscv64-gnu": "4.48.1", "@rollup/rollup-linux-riscv64-musl": "4.48.1", "@rollup/rollup-linux-s390x-gnu": "4.48.1", "@rollup/rollup-linux-x64-gnu": "4.48.1", "@rollup/rollup-linux-x64-musl": "4.48.1", "@rollup/rollup-win32-arm64-msvc": "4.48.1", "@rollup/rollup-win32-ia32-msvc": "4.48.1", "@rollup/rollup-win32-x64-msvc": "4.48.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
"tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
"tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="],
"tw-animate-css": ["tw-animate-css@1.3.7", "", {}, "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"valibot": ["valibot@0.42.1", "", { "peerDependencies": { "typescript": ">=5" } }, "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@7.1.11", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@tailwindcss/node/jiti": ["jiti@2.5.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ=="],
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="],
}
}

Binary file not shown.

View File

@@ -2,7 +2,7 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<link rel="manifest" href="./static/manifest.json" />
<link rel="manifest" href="./static/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/svg+xml" href="./static/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="robots" content="noindex, nofollow" />

View File

@@ -24,6 +24,7 @@ export default defineConfig({
"tr",
"ru",
"sl",
"sr",
"sv",
"uk",
"vi",

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.16.0",
"version": "0.17.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.16.0",
"version": "0.17.0",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -39,8 +39,8 @@
"lucide-react": "^0.452.0",
"nanostores": "^0.11.4",
"pocketbase": "^0.26.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "^19.1.2",
"react-dom": "^19.1.2",
"recharts": "^2.15.4",
"shiki": "^3.13.0",
"tailwind-merge": "^3.3.1",
@@ -111,6 +111,7 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -986,6 +987,29 @@
"integrity": "sha512-N3W7MKwTRmAxOjeG0NAT18oe2Xn3KdjkpMR6crbkF1UDamMGPjyigqEsefiv+qTaxibtc1a+zXCVzb9YXANVqw==",
"license": "MIT"
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1114,6 +1138,7 @@
"integrity": "sha512-9IO+PDvdneY8OCI8zvI1oDXpzryTMtyRv7uq9O0U1mFCvIPVd5dWQKQDu/CpgpYAc2+JG/izn5PNl9xzPc6ckw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.20.12",
"@babel/runtime": "^7.20.13",
@@ -1206,30 +1231,6 @@
"node": ">=14"
}
},
"node_modules/@lingui/cli/node_modules/glob": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@lingui/cli/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1243,65 +1244,6 @@
"node": ">= 6"
}
},
"node_modules/@lingui/cli/node_modules/jackspeak": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@lingui/cli/node_modules/lru-cache": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@lingui/cli/node_modules/minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@lingui/cli/node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@lingui/cli/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1350,6 +1292,7 @@
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-5.4.1.tgz",
"integrity": "sha512-4FeIh56PH5vziPg2BYo4XYWWOHE4XaY/XR8Jakwn0/qwtLpydWMNVpZOpGWi7nfPZtcLaJLmZKup6UNxEl1Pfw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "5.4.1"
@@ -3545,6 +3488,7 @@
"integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3555,6 +3499,7 @@
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3606,9 +3551,9 @@
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3680,13 +3625,6 @@
"node": ">=10"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3733,16 +3671,6 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -3776,6 +3704,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -4486,13 +4415,13 @@
}
},
"node_modules/foreground-child": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
"integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.0",
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
@@ -4536,6 +4465,30 @@
"node": ">=6"
}
},
"node_modules/glob": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4756,6 +4709,22 @@
"dev": true,
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@@ -4807,9 +4776,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -5180,9 +5149,9 @@
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
@@ -5326,6 +5295,22 @@
"node": ">=6"
}
},
"node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -5408,6 +5393,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
@@ -5567,6 +5553,33 @@
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.2.4",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -5590,6 +5603,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5731,24 +5745,26 @@
}
},
"node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz",
"integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz",
"integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.1"
"react": "^19.1.2"
}
},
"node_modules/react-is": {
@@ -6171,6 +6187,16 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -6191,16 +6217,6 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -6216,9 +6232,9 @@
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6283,7 +6299,8 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.2.3",
@@ -6405,6 +6422,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6639,6 +6657,7 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6790,6 +6809,23 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -6805,13 +6841,6 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6825,20 +6854,10 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.16.0",
"version": "0.18.0-beta.1",
"type": "module",
"scripts": {
"dev": "vite --host",
@@ -46,8 +46,8 @@
"lucide-react": "^0.452.0",
"nanostores": "^0.11.4",
"pocketbase": "^0.26.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "^19.1.2",
"react-dom": "^19.1.2",
"recharts": "^2.15.4",
"shiki": "^3.13.0",
"tailwind-merge": "^3.3.1",
@@ -77,4 +77,4 @@
"optionalDependencies": {
"@esbuild/linux-arm64": "^0.21.5"
}
}
}

View File

@@ -61,6 +61,11 @@ export const ActiveAlerts = () => {
<AlertDescription>
{alert.name === "Status" ? (
<Trans>Connection is down</Trans>
) : info.invert ? (
<Trans>
Below {alert.value}
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
</Trans>
) : (
<Trans>
Exceeds {alert.value}

View File

@@ -1,4 +1,4 @@
import { t } from "@lingui/core/macro"
import { msg, t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
@@ -36,6 +36,9 @@ import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/i
import { InputCopy } from "./ui/input-copy"
export function AddSystemButton({ className }: { className?: string }) {
if (isReadOnlyUser()) {
return null
}
const [open, setOpen] = useState(false)
const opened = useRef(false)
if (open) {
@@ -45,10 +48,7 @@ export function AddSystemButton({ className }: { className?: string }) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
className={cn("flex gap-1 max-xs:h-[2.4rem]", className, isReadOnlyUser() && "hidden")}
>
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
<PlusIcon className="h-4 w-4 -ms-1" />
<Trans>
Add <span className="hidden sm:inline">System</span>
@@ -124,6 +124,8 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
}
}
const systemTranslation = t`System`
return (
<DialogContent
className="w-[90%] sm:w-auto sm:ns-dialog max-w-full rounded-lg"
@@ -134,7 +136,11 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
<Tabs defaultValue={tab} onValueChange={setTab}>
<DialogHeader>
<DialogTitle className="mb-1 pb-1 max-w-100 truncate pr-8">
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
{system ? (
<Trans>Edit {{ foo: systemTranslation }}</Trans>
) : (
<Trans>Add {{ foo: systemTranslation }}</Trans>
)}
</DialogTitle>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="docker">Docker</TabsTrigger>

View File

@@ -245,13 +245,23 @@ export function AlertContent({
{!singleDescription && (
<div>
<p id={`v${name}`} className="text-sm block h-8">
<Trans>
Average exceeds{" "}
<strong className="text-foreground">
{value}
{alertData.unit}
</strong>
</Trans>
{alertData.invert ? (
<Trans>
Average drops below{" "}
<strong className="text-foreground">
{value}
{alertData.unit}
</strong>
</Trans>
) : (
<Trans>
Average exceeds{" "}
<strong className="text-foreground">
{value}
{alertData.unit}
</strong>
</Trans>
)}
</p>
<div className="flex gap-3">
<Slider

View File

@@ -11,6 +11,7 @@ import {
import { chartMargin, cn, formatShortDate } from "@/lib/utils"
import type { ChartData, SystemStatsRecord } from "@/types"
import { useYAxisWidth } from "./hooks"
import { AxisDomain } from "recharts/types/util/types"
export type DataPoint = {
label: string
@@ -41,7 +42,7 @@ export default function AreaChartDefault({
tickFormatter: (value: number, index: number) => string
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
dataPoints?: DataPoint[]
domain?: [number, number]
domain?: AxisDomain
legend?: boolean
showTotal?: boolean
itemSorter?: (a: any, b: any) => number

View File

@@ -2,7 +2,7 @@
import { useStore } from "@nanostores/react"
import { memo, useMemo } from "react"
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, pinnedAxisDomain, xAxis } from "@/components/ui/chart"
import { ChartType, Unit } from "@/lib/enums"
import { $containerFilter, $userSettings } from "@/lib/stores"
import { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from "@/lib/utils"
@@ -124,6 +124,7 @@ export default memo(function ContainerChart({
<CartesianGrid vertical={false} />
<YAxis
direction="ltr"
domain={pinnedAxisDomain()}
orientation={chartData.orientation}
className="tracking-tighter"
width={yAxisWidth}

View File

@@ -8,10 +8,11 @@ import {
ContainerIcon,
DatabaseBackupIcon,
FingerprintIcon,
LayoutDashboard,
HardDriveIcon,
LogsIcon,
MailIcon,
Server,
ServerIcon,
SettingsIcon,
UsersIcon,
} from "lucide-react"
@@ -81,15 +82,15 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
)}
<CommandGroup heading={t`Pages / Settings`}>
<CommandItem
keywords={["home", t`All Systems`]}
keywords={["home"]}
onSelect={() => {
navigate(basePath)
setOpen(false)
}}
>
<LayoutDashboard className="me-2 size-4" />
<ServerIcon className="me-2 size-4" />
<span>
<Trans>Dashboard</Trans>
<Trans>All Systems</Trans>
</span>
<CommandShortcut>
<Trans>Page</Trans>
@@ -109,6 +110,18 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
<Trans>Page</Trans>
</CommandShortcut>
</CommandItem>
<CommandItem
onSelect={() => {
navigate(getPagePath($router, "smart"))
setOpen(false)
}}
>
<HardDriveIcon className="me-2 size-4" />
<span>S.M.A.R.T.</span>
<CommandShortcut>
<Trans>Page</Trans>
</CommandShortcut>
</CommandItem>
<CommandItem
onSelect={() => {
navigate(getPagePath($router, "settings", { name: "general" }))

View File

@@ -26,7 +26,7 @@ import { Sheet, SheetTitle, SheetHeader, SheetContent, SheetDescription } from "
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog"
import { Button } from "@/components/ui/button"
import { $allSystemsById } from "@/lib/stores"
import { MaximizeIcon, RefreshCwIcon } from "lucide-react"
import { LoaderCircleIcon, MaximizeIcon, RefreshCwIcon, XIcon } from "lucide-react"
import { Separator } from "../ui/separator"
import { $router, Link } from "../router"
import { listenKeys } from "nanostores"
@@ -36,7 +36,7 @@ const syntaxTheme = "github-dark-dimmed"
export default function ContainersTable({ systemId }: { systemId?: string }) {
const loadTime = Date.now()
const [data, setData] = useState<ContainerRecord[]>([])
const [data, setData] = useState<ContainerRecord[] | undefined>(undefined)
const [sorting, setSorting] = useBrowserStorage<SortingState>(
`sort-c-${systemId ? 1 : 0}`,
[{ id: systemId ? "name" : "system", desc: false }],
@@ -54,23 +54,31 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
fields: "id,name,image,cpu,memory,net,health,status,system,updated",
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
})
.then(({ items }) => items.length && setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const containerIds = new Set()
const newItems = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
.then(
({ items }) => {
if (items.length === 0) {
setData([]);
return;
}
setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const containerIds = new Set()
const newItems = []
for (const item of items) {
if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
}
}
for (const item of curItems ?? []) {
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
})
}
for (const item of curItems) {
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
}))
)
}
// initial load
@@ -93,7 +101,7 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
}, [])
const table = useReactTable({
data,
data: data ?? [],
columns: containerChartCols.filter((col) => (systemId ? col.id !== "system" : true)),
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
@@ -147,16 +155,30 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
<Trans>Click on a container to view more information.</Trans>
</CardDescription>
</div>
<Input
placeholder={t`Filter...`}
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="ms-auto px-4 w-full max-w-full md:w-64"
/>
<div className="relative ms-auto w-full max-w-full md:w-64">
<Input
placeholder={t`Filter...`}
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="ps-4 pe-10 w-full"
/>
{globalFilter && (
<Button
type="button"
variant="ghost"
size="icon"
aria-label={t`Clear`}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground"
onClick={() => setGlobalFilter("")}
>
<XIcon className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CardHeader>
<div className="rounded-md">
<AllContainersTable table={table} rows={rows} colLength={visibleColumns.length} />
<AllContainersTable table={table} rows={rows} colLength={visibleColumns.length} data={data} />
</div>
</Card>
)
@@ -166,10 +188,12 @@ const AllContainersTable = memo(function AllContainersTable({
table,
rows,
colLength,
data,
}: {
table: TableType<ContainerRecord>
rows: Row<ContainerRecord>[]
colLength: number
data: ContainerRecord[] | undefined
}) {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
@@ -213,7 +237,11 @@ const AllContainersTable = memo(function AllContainersTable({
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No results.</Trans>
{data ? (
<Trans>No results.</Trans>
) : (
<LoaderCircleIcon className="animate-spin size-10 opacity-60 mx-auto" />
)}
</TableCell>
</TableRow>
)}
@@ -252,7 +280,7 @@ async function getInfoHtml(container: ContainerRecord): Promise<string> {
])
try {
info = JSON.stringify(JSON.parse(info), null, 2)
} catch (_) { }
} catch (_) {}
return info ? highlighter.codeToHtml(info, { lang: "json", theme: syntaxTheme }) : t`No results.`
} catch (error) {
console.error(error)
@@ -309,7 +337,7 @@ function ContainerSheet({
setLogsDisplay("")
setInfoDisplay("")
if (!container) return
; (async () => {
;(async () => {
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
setLogsDisplay(logsHtml)
setInfoDisplay(infoHtml)
@@ -491,9 +519,7 @@ function LogsFullscreenDialog({
</div>
</div>
<button
onClick={() => {
void onRefresh()
}}
onClick={onRefresh}
className="absolute top-3 right-11 opacity-60 hover:opacity-100 p-1"
disabled={isRefreshing}
title={t`Refresh`}

View File

@@ -3,6 +3,7 @@ import { getPagePath } from "@nanostores/router"
import {
ContainerIcon,
DatabaseBackupIcon,
HardDriveIcon,
LogOutIcon,
LogsIcon,
SearchIcon,
@@ -29,6 +30,7 @@ import { LangToggle } from "./lang-toggle"
import { Logo } from "./logo"
import { ModeToggle } from "./mode-toggle"
import { $router, basePath, Link, prependBasePath } from "./router"
import { t } from "@lingui/core/macro"
const CommandPalette = lazy(() => import("./command-palette"))
@@ -55,6 +57,13 @@ export default function Navbar() {
>
<ContainerIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
</Link>
<Link
href={getPagePath($router, "smart")}
className={cn("hidden md:grid", buttonVariants({ variant: "ghost", size: "icon" }))}
aria-label="S.M.A.R.T."
>
<HardDriveIcon className="h-[1.2rem] w-[1.2rem]" strokeWidth={1.5} />
</Link>
<LangToggle />
<ModeToggle />
<Link

View File

@@ -3,6 +3,7 @@ import { createRouter } from "@nanostores/router"
const routes = {
home: "/",
containers: "/containers",
smart: "/smart",
system: `/system/:id`,
settings: `/settings/:name?`,
forgot_password: `/forgot-password`,

View File

@@ -7,6 +7,7 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type PaginationState,
type SortingState,
useReactTable,
type VisibilityState,
@@ -40,7 +41,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { useToast } from "@/components/ui/use-toast"
import { alertInfo } from "@/lib/alerts"
import { pb } from "@/lib/api"
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
import { cn, formatDuration, formatShortDate, useBrowserStorage } from "@/lib/utils"
import type { AlertsHistoryRecord } from "@/types"
import { alertsHistoryColumns } from "../../alerts-history-columns"
@@ -66,6 +67,12 @@ export default function AlertsHistoryDataTable() {
const [globalFilter, setGlobalFilter] = useState("")
const { toast } = useToast()
const [deleteOpen, setDeleteDialogOpen] = useState(false)
// Store pagination preference in local storage
const [pagination, setPagination] = useBrowserStorage<PaginationState>("ah-pagination", {
pageIndex: 0,
pageSize: 10,
})
useEffect(() => {
let unsubscribe: (() => void) | undefined
@@ -136,12 +143,14 @@ export default function AlertsHistoryDataTable() {
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
globalFilter,
pagination,
},
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: (row, _columnId, filterValue) => {
@@ -318,10 +327,10 @@ export default function AlertsHistoryDataTable() {
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="w-[4.8em]" id="rows-per-page">
<SelectTrigger className="w-18" id="rows-per-page">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">

View File

@@ -2,14 +2,17 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { LanguagesIcon, LoaderCircleIcon, SaveIcon } from "lucide-react"
import { useState } from "react"
import { useStore } from "@nanostores/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import Slider from "@/components/ui/slider"
import { HourFormat, Unit } from "@/lib/enums"
import { dynamicActivate } from "@/lib/i18n"
import languages from "@/lib/languages"
import { $userSettings } from "@/lib/stores"
import { chartTimeData, currentHour12 } from "@/lib/utils"
import type { UserSettings } from "@/types"
import { saveSettings } from "./layout"
@@ -17,6 +20,8 @@ import { saveSettings } from "./layout"
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
const [isLoading, setIsLoading] = useState(false)
const { i18n } = useLingui()
const currentUserSettings = useStore($userSettings)
const layoutWidth = currentUserSettings.layoutWidth ?? 1500
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
@@ -73,6 +78,27 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
</Select>
</div>
<Separator />
<div className="grid gap-2">
<div className="mb-2">
<h3 className="mb-1 text-lg font-medium">
<Trans>Layout width</Trans>
</h3>
<Label htmlFor="layoutWidth" className="text-sm text-muted-foreground leading-relaxed">
<Trans>Adjust the width of the main layout</Trans> ({layoutWidth}px)
</Label>
</div>
<Slider
id="layoutWidth"
name="layoutWidth"
value={[layoutWidth]}
onValueChange={(val) => $userSettings.setKey("layoutWidth", val[0])}
min={1000}
max={2000}
step={10}
className="w-full mb-1"
/>
</div>
<Separator />
<div className="grid gap-2">
<div className="mb-2">
<h3 className="mb-1 text-lg font-medium">

View File

@@ -14,6 +14,7 @@ import { toast } from "@/components/ui/use-toast"
import { isAdmin, pb } from "@/lib/api"
import type { UserSettings } from "@/types"
import { saveSettings } from "./layout"
import { QuietHours } from "./quiet-hours"
interface ShoutrrrUrlCardProps {
url: string
@@ -120,19 +121,32 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
</div>
<Separator />
<div className="space-y-3">
<div>
<h3 className="mb-1 text-lg font-medium">
<Trans>Webhook / Push notifications</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
Beszel uses{" "}
<a href="https://beszel.dev/guide/notifications" target="_blank" className="link" rel="noopener">
Shoutrrr
</a>{" "}
to integrate with popular notification services.
</Trans>
</p>
<div className="grid grid-cols-1 sm:flex items-center justify-between gap-4">
<div>
<h3 className="mb-1 text-lg font-medium">
<Trans>Webhook / Push notifications</Trans>
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
Beszel uses{" "}
<a href="https://beszel.dev/guide/notifications" target="_blank" className="link" rel="noopener">
Shoutrrr
</a>{" "}
to integrate with popular notification services.
</Trans>
</p>
</div>
<Button
type="button"
variant="outline"
className="h-10 shrink-0"
onClick={addWebhook}
>
<PlusIcon className="size-4" />
<span className="ms-1">
<Trans>Add URL</Trans>
</span>
</Button>
</div>
{webhooks.length > 0 && (
<div className="grid gap-2.5" id="webhooks">
@@ -146,16 +160,10 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
))}
</div>
)}
<Button
type="button"
variant="outline"
size="sm"
className="mt-2 flex items-center gap-1"
onClick={addWebhook}
>
<PlusIcon className="h-4 w-4 -ms-0.5" />
<Trans>Add URL</Trans>
</Button>
</div>
<Separator />
<div className="space-y-3">
<QuietHours />
</div>
<Separator />
<Button
@@ -194,7 +202,7 @@ const ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) =
}
return (
<Card className="bg-muted/40 p-2 md:p-3">
<Card className="bg-table-header p-2 md:p-3">
<div className="flex items-center gap-1">
<Input
type="url"

View File

@@ -0,0 +1,534 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import {
MoreHorizontalIcon,
PlusIcon,
Trash2Icon,
ServerIcon,
ClockIcon,
CalendarIcon,
ActivityIcon,
PenSquareIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useToast } from "@/components/ui/use-toast"
import { pb } from "@/lib/api"
import { $systems } from "@/lib/stores"
import { formatShortDate } from "@/lib/utils"
import type { QuietHoursRecord, SystemRecord } from "@/types"
const quietHoursTranslation = t`Quiet Hours`
export function QuietHours() {
const [data, setData] = useState<QuietHoursRecord[]>([])
const [dialogOpen, setDialogOpen] = useState(false)
const [editingRecord, setEditingRecord] = useState<QuietHoursRecord | null>(null)
const { toast } = useToast()
const systems = useStore($systems)
useEffect(() => {
let unsubscribe: (() => void) | undefined
const pbOptions = {
expand: "system",
fields: "id,user,system,type,start,end,expand.system.name",
}
// Initial load
pb.collection<QuietHoursRecord>("quiet_hours")
.getList(0, 200, {
...pbOptions,
sort: "system",
})
.then(({ items }) => setData(items))
// Subscribe to changes
;(async () => {
unsubscribe = await pb.collection("quiet_hours").subscribe(
"*",
(e) => {
if (e.action === "create") {
setData((current) => [e.record as QuietHoursRecord, ...current])
}
if (e.action === "update") {
setData((current) => current.map((r) => (r.id === e.record.id ? (e.record as QuietHoursRecord) : r)))
}
if (e.action === "delete") {
setData((current) => current.filter((r) => r.id !== e.record.id))
}
},
pbOptions
)
})()
// Unsubscribe on unmount
return () => unsubscribe?.()
}, [])
const handleDelete = async (id: string) => {
try {
await pb.collection("quiet_hours").delete(id)
} catch (e: unknown) {
toast({
variant: "destructive",
title: t`Error`,
description: (e as Error).message || "Failed to delete quiet hours.",
})
}
}
const openEditDialog = (record: QuietHoursRecord) => {
setEditingRecord(record)
setDialogOpen(true)
}
const closeDialog = () => {
setDialogOpen(false)
setEditingRecord(null)
}
const formatDateTime = (record: QuietHoursRecord) => {
if (record.type === "daily") {
// For daily windows, show only time
const startTime = new Date(record.start).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
const endTime = new Date(record.end).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
return `${startTime} - ${endTime}`
}
// For one-time windows, show full date and time
const start = formatShortDate(record.start)
const end = formatShortDate(record.end)
return `${start} - ${end}`
}
const getWindowState = (record: QuietHoursRecord): "active" | "past" | "inactive" => {
const now = new Date()
if (record.type === "daily") {
// For daily windows, check if current time is within the window
const startDate = new Date(record.start)
const endDate = new Date(record.end)
// Get current time in local timezone
const currentMinutes = now.getHours() * 60 + now.getMinutes()
const startMinutes = startDate.getUTCHours() * 60 + startDate.getUTCMinutes()
const endMinutes = endDate.getUTCHours() * 60 + endDate.getUTCMinutes()
// Convert UTC to local time offset
const offset = now.getTimezoneOffset()
const localStartMinutes = (startMinutes - offset + 1440) % 1440
const localEndMinutes = (endMinutes - offset + 1440) % 1440
// Handle cases where window spans midnight
if (localStartMinutes <= localEndMinutes) {
return currentMinutes >= localStartMinutes && currentMinutes < localEndMinutes ? "active" : "inactive"
} else {
return currentMinutes >= localStartMinutes || currentMinutes < localEndMinutes ? "active" : "inactive"
}
} else {
// For one-time windows
const startDate = new Date(record.start)
const endDate = new Date(record.end)
if (now >= startDate && now < endDate) {
return "active"
} else if (now >= endDate) {
return "past"
} else {
return "inactive"
}
}
}
return (
<>
<div className="grid grid-cols-1 sm:flex items-center justify-between gap-4 mb-3">
<div>
<h3 className="mb-1 text-lg font-medium">{quietHoursTranslation}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
<Trans>
Schedule quiet hours where notifications will not be sent, such as during maintenance periods.
</Trans>
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="h-10 shrink-0" onClick={() => setEditingRecord(null)}>
<PlusIcon className="size-4" />
<span className="ms-1">
<Trans>Add {{ foo: quietHoursTranslation }}</Trans>
</span>
</Button>
</DialogTrigger>
<QuietHoursDialog editingRecord={editingRecord} systems={systems} onClose={closeDialog} toast={toast} />
</Dialog>
</div>
{data.length > 0 && (
<div className="rounded-md border overflow-x-auto whitespace-nowrap">
<Table>
<TableHeader>
<TableRow className="border-border/50">
<TableHead className="px-4">
<span className="flex items-center gap-2">
<ServerIcon className="size-4" />
<Trans>System</Trans>
</span>
</TableHead>
<TableHead className="px-4">
<span className="flex items-center gap-2">
<ClockIcon className="size-4" />
<Trans>Type</Trans>
</span>
</TableHead>
<TableHead className="px-4">
<span className="flex items-center gap-2">
<CalendarIcon className="size-4" />
<Trans>Schedule</Trans>
</span>
</TableHead>
<TableHead className="px-4">
<span className="flex items-center gap-2">
<ActivityIcon className="size-4" />
<Trans>State</Trans>
</span>
</TableHead>
<TableHead className="px-4 text-right sr-only">
<Trans>Actions</Trans>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((record) => (
<TableRow key={record.id}>
<TableCell className="px-4 py-3">
{record.system ? record.expand?.system?.name || record.system : <Trans>All Systems</Trans>}
</TableCell>
<TableCell className="px-4 py-3">
{record.type === "daily" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>}
</TableCell>
<TableCell className="px-4 py-3">{formatDateTime(record)}</TableCell>
<TableCell className="px-4 py-3">
{(() => {
const state = getWindowState(record)
const stateConfig = {
active: { label: <Trans>Active</Trans>, variant: "success" as const },
past: { label: <Trans>Past</Trans>, variant: "danger" as const },
inactive: { label: <Trans>Inactive</Trans>, variant: "default" as const },
}
const config = stateConfig[state]
return <Badge variant={config.variant}>{config.label}</Badge>
})()}
</TableCell>
<TableCell className="px-4 py-3 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<span className="sr-only">
<Trans>Open menu</Trans>
</span>
<MoreHorizontalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openEditDialog(record)}>
<PenSquareIcon className="me-2.5 size-4" />
<Trans>Edit</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleDelete(record.id)}>
<Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</>
)
}
// Helper function to format Date as datetime-local string (YYYY-MM-DDTHH:mm) in local time
function formatDateTimeLocal(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
const hours = String(date.getHours()).padStart(2, "0")
const minutes = String(date.getMinutes()).padStart(2, "0")
return `${year}-${month}-${day}T${hours}:${minutes}`
}
function QuietHoursDialog({
editingRecord,
systems,
onClose,
toast,
}: {
editingRecord: QuietHoursRecord | null
systems: SystemRecord[]
onClose: () => void
toast: ReturnType<typeof useToast>["toast"]
}) {
const [selectedSystem, setSelectedSystem] = useState(editingRecord?.system || "")
const [isGlobal, setIsGlobal] = useState(!editingRecord?.system)
const [windowType, setWindowType] = useState<"one-time" | "daily">(editingRecord?.type || "one-time")
const [startDateTime, setStartDateTime] = useState("")
const [endDateTime, setEndDateTime] = useState("")
const [startTime, setStartTime] = useState("")
const [endTime, setEndTime] = useState("")
useEffect(() => {
if (editingRecord) {
setSelectedSystem(editingRecord.system || "")
setIsGlobal(!editingRecord.system)
setWindowType(editingRecord.type)
if (editingRecord.type === "daily") {
// Extract time from datetime
const start = new Date(editingRecord.start)
const end = editingRecord.end ? new Date(editingRecord.end) : null
setStartTime(start.toTimeString().slice(0, 5))
setEndTime(end ? end.toTimeString().slice(0, 5) : "")
} else {
// For one-time, format as datetime-local (local time, not UTC)
const startDate = new Date(editingRecord.start)
const endDate = editingRecord.end ? new Date(editingRecord.end) : null
setStartDateTime(formatDateTimeLocal(startDate))
setEndDateTime(endDate ? formatDateTimeLocal(endDate) : "")
}
} else {
// Reset form with default dates: today at 12pm and 1pm
const today = new Date()
const noon = new Date(today)
noon.setHours(12, 0, 0, 0)
const onePm = new Date(today)
onePm.setHours(13, 0, 0, 0)
setSelectedSystem("")
setIsGlobal(true)
setWindowType("one-time")
setStartDateTime(formatDateTimeLocal(noon))
setEndDateTime(formatDateTimeLocal(onePm))
setStartTime("12:00")
setEndTime("13:00")
}
}, [editingRecord])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
let startValue: string
let endValue: string | undefined
if (windowType === "daily") {
// For daily windows, convert local time to UTC
// Create a date with the time in local timezone, then convert to UTC
const startDate = new Date(`2000-01-01T${startTime}:00`)
startValue = startDate.toISOString()
if (endTime) {
const endDate = new Date(`2000-01-01T${endTime}:00`)
endValue = endDate.toISOString()
}
} else {
// For one-time windows, use the datetime values
startValue = new Date(startDateTime).toISOString()
endValue = endDateTime ? new Date(endDateTime).toISOString() : undefined
}
const data = {
user: pb.authStore.record?.id,
system: isGlobal ? undefined : selectedSystem,
type: windowType,
start: startValue,
end: endValue,
}
if (editingRecord) {
await pb.collection("quiet_hours").update(editingRecord.id, data)
} else {
await pb.collection("quiet_hours").create(data)
}
onClose()
} catch (e) {
toast({
variant: "destructive",
title: t`Error`,
description: t`Failed to save settings`,
})
}
}
return (
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingRecord ? (
<Trans>Edit {{ foo: quietHoursTranslation }}</Trans>
) : (
<Trans>Add {{ foo: quietHoursTranslation }}</Trans>
)}
</DialogTitle>
<DialogDescription>
<Trans>Schedule quiet hours where notifications will not be sent.</Trans>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<Tabs value={isGlobal ? "global" : "system"} onValueChange={(value) => setIsGlobal(value === "global")}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="global">
<Trans>Global</Trans>
</TabsTrigger>
<TabsTrigger value="system">
<Trans>System</Trans>
</TabsTrigger>
</TabsList>
<TabsContent value="system" className="mt-4 space-y-4">
<div className="grid gap-2">
<Label htmlFor="system">
<Trans>System</Trans>
</Label>
<Select value={selectedSystem} onValueChange={setSelectedSystem}>
<SelectTrigger id="system">
<SelectValue placeholder={t`Select ${{ foo: t`System`.toLocaleLowerCase() }}`} />
</SelectTrigger>
<SelectContent>
{systems.map((system) => (
<SelectItem key={system.id} value={system.id}>
{system.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Hidden input for native form validation */}
<input
className="sr-only"
type="text"
tabIndex={-1}
autoComplete="off"
value={selectedSystem}
onChange={() => {}}
required={!isGlobal}
/>
</div>
</TabsContent>
</Tabs>
<div className="grid gap-2">
<Label htmlFor="type">
<Trans>Type</Trans>
</Label>
<Select value={windowType} onValueChange={(value: "one-time" | "daily") => setWindowType(value)}>
<SelectTrigger id="type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="one-time">
<Trans>One-time</Trans>
</SelectItem>
<SelectItem value="daily">
<Trans>Daily</Trans>
</SelectItem>
</SelectContent>
</Select>
</div>
{windowType === "one-time" ? (
<>
<div className="grid gap-2">
<Label htmlFor="start-datetime">
<Trans>Start Time</Trans>
</Label>
<Input
id="start-datetime"
type="datetime-local"
value={startDateTime}
onChange={(e) => setStartDateTime(e.target.value)}
min={formatDateTimeLocal(new Date(new Date().setHours(0, 0, 0, 0)))}
required
className="tabular-nums tracking-tighter"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="end-datetime">
<Trans>End Time</Trans>
</Label>
<Input
id="end-datetime"
type="datetime-local"
value={endDateTime}
onChange={(e) => setEndDateTime(e.target.value)}
min={startDateTime || formatDateTimeLocal(new Date())}
required
className="tabular-nums tracking-tighter"
/>
</div>
</>
) : (
<div className="grid gap-2 grid-cols-2">
<div>
<Label htmlFor="start-time">
<Trans>Start Time</Trans>
</Label>
<Input
className="tabular-nums tracking-tighter"
id="start-time"
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="end-time">
<Trans>End Time</Trans>
</Label>
<Input
className="tabular-nums tracking-tighter"
id="end-time"
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
required
/>
</div>
</div>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit">{editingRecord ? <Trans>Update</Trans> : <Trans>Create</Trans>}</Button>
</DialogFooter>
</form>
</DialogContent>
)
}

View File

@@ -0,0 +1,20 @@
import { useEffect } from "react"
import SmartTable from "@/components/routes/system/smart-table"
import { ActiveAlerts } from "@/components/active-alerts"
import { FooterRepoLink } from "@/components/footer-repo-link"
export default function Smart() {
useEffect(() => {
document.title = `S.M.A.R.T. / Beszel`
}, [])
return (
<>
<div className="grid gap-4">
<ActiveAlerts />
<SmartTable />
</div>
<FooterRepoLink />
</>
)
}

View File

@@ -3,15 +3,7 @@ import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router"
import { timeTicks } from "d3-time"
import {
ChevronRightSquareIcon,
ClockArrowUp,
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MonitorIcon,
XIcon,
} from "lucide-react"
import { XIcon } from "lucide-react"
import { subscribeKeys } from "nanostores"
import React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import AreaChartDefault, { type DataPoint } from "@/components/charts/area-chart"
@@ -24,7 +16,7 @@ import MemChart from "@/components/charts/mem-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import { getPbTimestamp, pb } from "@/lib/api"
import { ChartType, ConnectionType, connectionTypeLabels, Os, SystemStatus, Unit } from "@/lib/enums"
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums"
import { batteryStateTranslations } from "@/lib/i18n"
import {
$allSystemsById,
@@ -44,8 +36,6 @@ import {
compareSemVer,
decimalString,
formatBytes,
secondsToString,
getHostDisplayValue,
listen,
parseSemVer,
toFixedFloat,
@@ -56,24 +46,24 @@ import type {
ChartTimes,
ContainerStatsRecord,
GPUData,
SystemDetailsRecord,
SystemInfo,
SystemRecord,
SystemStats,
SystemStatsRecord,
} from "@/types"
import ChartTimeSelect from "../charts/chart-time-select"
import { $router, navigate } from "../router"
import Spinner from "../spinner"
import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { ChartAverage, ChartMax } from "../ui/icons"
import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import NetworkSheet from "./system/network-sheet"
import CpuCoresSheet from "./system/cpu-sheet"
import LineChartDefault from "../charts/line-chart"
import { pinnedAxisDomain } from "../ui/chart"
import InfoBar from "./system/info-bar"
type ChartTimeData = {
time: number
@@ -153,8 +143,8 @@ async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
})
}
function dockerOrPodman(str: string, system: SystemRecord): string {
if (system.info.p) {
function dockerOrPodman(str: string, isPodman: boolean): string {
if (isPodman) {
return str.replace("docker", "podman").replace("Docker", "Podman")
}
return str
@@ -177,6 +167,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const isLongerChart = !["1m", "1h"].includes(chartTime) // true if chart time is not 1m or 1h
const userSettings = $userSettings.get()
const chartWrapRef = useRef<HTMLDivElement>(null)
const [details, setDetails] = useState<SystemDetailsRecord>({} as SystemDetailsRecord)
useEffect(() => {
return () => {
@@ -186,6 +177,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
persistChartTime.current = false
setSystemStats([])
setContainerData([])
setDetails({} as SystemDetailsRecord)
$containerFilter.set("")
}
}, [id])
@@ -213,10 +205,26 @@ export default memo(function SystemDetail({ id }: { id: string }) {
}
}, [system?.info?.v])
// fetch system details
useEffect(() => {
// if system.info.m exists, agent is old version without system details
if (!system.id || system.info?.m) {
return
}
pb.collection<SystemDetailsRecord>("system_details")
.getOne(system.id, {
fields: "hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman",
headers: {
"Cache-Control": "public, max-age=60",
},
})
.then(setDetails)
}, [system.id])
// subscribe to realtime metrics if chart time is 1m
// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary
useEffect(() => {
let unsub = () => { }
let unsub = () => {}
if (!system.id || chartTime !== "1m") {
return
}
@@ -332,63 +340,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
})
}, [system, chartTime])
// values for system info bar
const systemInfo = useMemo(() => {
if (!system.info) {
return []
}
const osInfo = {
[Os.Linux]: {
Icon: TuxIcon,
value: system.info.k,
label: t({ comment: "Linux kernel", message: "Kernel" }),
},
[Os.Darwin]: {
Icon: AppleIcon,
value: `macOS ${system.info.k}`,
},
[Os.Windows]: {
Icon: WindowsIcon,
value: system.info.k,
},
[Os.FreeBSD]: {
Icon: FreeBsdIcon,
value: system.info.k,
},
}
let uptime: string
if (system.info.u < 3600) {
uptime = secondsToString(system.info.u, "minute")
} else if (system.info.u < 360000) {
uptime = secondsToString(system.info.u, "hour")
} else {
uptime = secondsToString(system.info.u, "day")
}
return [
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
{
value: system.info.h,
Icon: MonitorIcon,
label: "Hostname",
// hide if hostname is same as host or name
hide: system.info.h === system.host || system.info.h === system.name,
},
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[system.info.os ?? Os.Linux],
{
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`,
Icon: CpuIcon,
hide: !system.info.m,
},
] as {
value: string | number | undefined
label?: string
Icon: React.ElementType
hide?: boolean
}[]
}, [system, t])
/** Space for tooltip if more than 10 sensors and no containers table */
useEffect(() => {
const sensors = Object.keys(systemStats.at(-1)?.stats.t ?? {})
@@ -456,114 +407,14 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const hasGpuData = lastGpuVals.length > 0
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined || gpu.pp !== undefined)
const hasGpuEnginesData = lastGpuVals.some((gpu) => gpu.e !== undefined)
let translatedStatus: string = system.status
if (system.status === SystemStatus.Up) {
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
} else if (system.status === SystemStatus.Down) {
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
}
const isLinux = (details?.os ?? system.info?.os) === Os.Linux
const isPodman = details?.podman ?? system.info?.p ?? false
return (
<>
<div ref={chartWrapRef} className="grid gap-4 mb-14 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
<div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="capitalize flex gap-2 items-center">
<span className={cn("relative flex h-3 w-3")}>
{system.status === SystemStatus.Up && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
)}
<span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</TooltipTrigger>
{system.info.ct && (
<TooltipContent>
<div className="flex gap-1 items-center">
{system.info.ct === ConnectionType.WebSocket ? (
<WebSocketIcon className="size-4" />
) : (
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
)}
{connectionTypeLabels[system.info.ct as ConnectionType]}
</div>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) {
return null
}
const content = (
<div className="flex gap-1.5 items-center">
<Icon className="h-4 w-4" /> {value}
</div>
)
return (
<div key={value} className="contents">
<Separator orientation="vertical" className="h-4 bg-primary/30" />
{label ? (
<TooltipProvider>
<Tooltip delayDuration={150}>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
content
)}
</div>
)
})}
</div>
</div>
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t`Toggle grid`}
variant="outline"
size="icon"
className="hidden xl:flex p-0 text-primary"
onClick={() => setGrid(!grid)}
>
{grid ? (
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
) : (
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{t`Toggle grid`}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</Card>
<InfoBar system={system} chartData={chartData} grid={grid} setGrid={setGrid} details={details} />
{/* <Tabs defaultValue="overview" className="w-full">
<TabsList className="w-full h-11">
@@ -575,7 +426,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</TabsContent>
</Tabs> */}
{/* main charts */}
<div className="grid xl:grid-cols-2 gap-4">
<ChartCard
@@ -603,6 +453,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
]}
tickFormatter={(val) => `${toFixedFloat(val, 2)}%`}
contentFormatter={({ value }) => `${decimalString(value)}%`}
domain={pinnedAxisDomain()}
/>
</ChartCard>
@@ -610,7 +461,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker CPU Usage`, system)}
title={dockerOrPodman(t`Docker CPU Usage`, isPodman)}
description={t`Average CPU utilization of containers`}
cornerEl={containerFilterBar}
>
@@ -637,8 +488,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Memory Usage`, system)}
description={dockerOrPodman(t`Memory usage of docker containers`, system)}
title={dockerOrPodman(t`Docker Memory Usage`, isPodman)}
description={dockerOrPodman(t`Memory usage of docker containers`, isPodman)}
cornerEl={containerFilterBar}
>
<ContainerChart
@@ -758,8 +609,8 @@ export default memo(function SystemDetail({ id }: { id: string }) {
<ChartCard
empty={dataEmpty}
grid={grid}
title={dockerOrPodman(t`Docker Network I/O`, system)}
description={dockerOrPodman(t`Network traffic of docker containers`, system)}
title={dockerOrPodman(t`Docker Network I/O`, isPodman)}
description={dockerOrPodman(t`Network traffic of docker containers`, isPodman)}
cornerEl={containerFilterBar}
>
<ContainerChart
@@ -798,10 +649,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{/* Temperature chart */}
{systemStats.at(-1)?.stats.t && (
<div
ref={temperatureChartRef}
className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}
>
<div ref={temperatureChartRef} className={cn("odd:last-of-type:col-span-full", { "col-span-full": !grid })}>
<ChartCard
empty={dataEmpty}
grid={grid}
@@ -963,7 +811,9 @@ export default memo(function SystemDetail({ id }: { id: string }) {
label: t`Write`,
dataKey: ({ stats }) => {
if (showMax) {
return stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
return (
stats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024
)
}
return stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024
},
@@ -1001,15 +851,13 @@ export default memo(function SystemDetail({ id }: { id: string }) {
</div>
)}
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && (
<LazySmartTable systemId={system.id} />
)}
{compareSemVer(chartData.agentVersion, parseSemVer("0.15.0")) >= 0 && <LazySmartTable systemId={system.id} />}
{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer("0.14.0")) >= 0 && (
<LazyContainersTable systemId={system.id} />
)}
{system.info?.os === Os.Linux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
{isLinux && compareSemVer(chartData.agentVersion, parseSemVer("0.16.0")) >= 0 && (
<LazySystemdTable systemId={system.id} />
)}
</div>
@@ -1059,13 +907,10 @@ function FilterBar({ store = $containerFilter }: { store?: typeof $containerFilt
return () => clearTimeout(handle)
}, [inputValue, storeValue, store])
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
},
[]
)
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInputValue(value)
}, [])
const handleClear = useCallback(() => {
setInputValue("")
@@ -1192,4 +1037,4 @@ function LazySystemdTable({ systemId }: { systemId: string }) {
{isIntersecting && <SystemdTable systemId={systemId} />}
</div>
)
}
}

View File

@@ -0,0 +1,229 @@
import { plural } from "@lingui/core/macro"
import { useLingui } from "@lingui/react/macro"
import {
AppleIcon,
ChevronRightSquareIcon,
ClockArrowUp,
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MemoryStickIcon,
MonitorIcon,
Rows,
} from "lucide-react"
import { useMemo } from "react"
import ChartTimeSelect from "@/components/charts/chart-time-select"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from "@/components/ui/icons"
import { Separator } from "@/components/ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { ConnectionType, connectionTypeLabels, Os, SystemStatus } from "@/lib/enums"
import { cn, formatBytes, getHostDisplayValue, secondsToString, toFixedFloat } from "@/lib/utils"
import type { ChartData, SystemDetailsRecord, SystemRecord } from "@/types"
export default function InfoBar({
system,
chartData,
grid,
setGrid,
details,
}: {
system: SystemRecord
chartData: ChartData
grid: boolean
setGrid: (grid: boolean) => void
details: SystemDetailsRecord | null
}) {
const { t } = useLingui()
// values for system info bar - use details with fallback to system.info
const systemInfo = useMemo(() => {
if (!system.info) {
return []
}
// Use details if available, otherwise fall back to system.info
const hostname = details?.hostname ?? system.info.h
const kernel = details?.kernel ?? system.info.k
const cores = details?.cores ?? system.info.c
const threads = details?.threads ?? system.info.t ?? 0
const cpuModel = details?.cpu ?? system.info.m
const os = details?.os ?? system.info.os ?? Os.Linux
const osName = details?.os_name
const arch = details?.arch
const memory = details?.memory
const osInfo = {
[Os.Linux]: {
Icon: TuxIcon,
// show kernel in tooltip if os name is available, otherwise show the kernel
value: osName || kernel,
label: osName ? kernel : undefined,
},
[Os.Darwin]: {
Icon: AppleIcon,
value: osName || `macOS ${kernel}`,
},
[Os.Windows]: {
Icon: WindowsIcon,
value: osName || kernel,
label: osName ? kernel : undefined,
},
[Os.FreeBSD]: {
Icon: FreeBsdIcon,
value: osName || kernel,
label: osName ? kernel : undefined,
},
}
let uptime: string
if (system.info.u < 3600) {
uptime = secondsToString(system.info.u, "minute")
} else if (system.info.u < 360000) {
uptime = secondsToString(system.info.u, "hour")
} else {
uptime = secondsToString(system.info.u, "day")
}
const info = [
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
{
value: hostname,
Icon: MonitorIcon,
label: "Hostname",
// hide if hostname is same as host or name
hide: hostname === system.host || hostname === system.name,
},
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[os],
{
value: cpuModel,
Icon: CpuIcon,
hide: !cpuModel,
label: `${plural(cores, { one: "# core", other: "# cores" })} / ${plural(threads, { one: "# thread", other: "# threads" })}${arch ? ` / ${arch}` : ""}`,
},
] as {
value: string | number | undefined
label?: string
Icon: React.ElementType
hide?: boolean
}[]
if (memory) {
const memValue = formatBytes(memory, false, undefined, false)
info.push({
value: `${toFixedFloat(memValue.value, memValue.value >= 10 ? 1 : 2)} ${memValue.unit}`,
Icon: MemoryStickIcon,
hide: !memory,
label: t`Memory`,
})
}
return info
}, [system, details, t])
let translatedStatus: string = system.status
if (system.status === SystemStatus.Up) {
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
} else if (system.status === SystemStatus.Down) {
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
}
return (
<Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
<div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="capitalize flex gap-2 items-center">
<span className={cn("relative flex h-3 w-3")}>
{system.status === SystemStatus.Up && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
)}
<span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</TooltipTrigger>
{system.info.ct && (
<TooltipContent>
<div className="flex gap-1 items-center">
{system.info.ct === ConnectionType.WebSocket ? (
<WebSocketIcon className="size-4" />
) : (
<ChevronRightSquareIcon className="size-4" strokeWidth={2} />
)}
{connectionTypeLabels[system.info.ct as ConnectionType]}
</div>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) {
return null
}
const content = (
<div className="flex gap-1.5 items-center">
<Icon className="h-4 w-4" /> {value}
</div>
)
return (
<div key={value} className="contents">
<Separator orientation="vertical" className="h-4 bg-primary/30" />
{label ? (
<TooltipProvider>
<Tooltip delayDuration={150}>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
content
)}
</div>
)
})}
</div>
</div>
<div className="xl:ms-auto flex items-center gap-2 max-sm:-mb-1">
<ChartTimeSelect className="w-full xl:w-40" agentVersion={chartData.agentVersion} />
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={t`Toggle grid`}
variant="outline"
size="icon"
className="hidden xl:flex p-0 text-primary"
onClick={() => setGrid(!grid)}
>
{grid ? (
<LayoutGridIcon className="h-[1.2rem] w-[1.2rem] opacity-75" />
) : (
<Rows className="h-[1.3rem] w-[1.3rem] opacity-75" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{t`Toggle grid`}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</Card>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/** biome-ignore-all lint/correctness/useHookAtTopLevel: Hooks live inside memoized column definitions */
import { t } from "@lingui/core/macro"
import { Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react"
@@ -16,13 +17,14 @@ import {
PenBoxIcon,
PlayCircleIcon,
ServerIcon,
TerminalSquareIcon,
Trash2Icon,
WifiIcon,
} from "lucide-react"
import { memo, useMemo, useRef, useState } from "react"
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
import { isReadOnlyUser, pb } from "@/lib/api"
import { ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums"
import { BatteryState, ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import {
cn,
@@ -33,6 +35,7 @@ import {
getMeterState,
parseSemVer,
} from "@/lib/utils"
import { batteryStateTranslations } from "@/lib/i18n"
import type { SystemRecord } from "@/types"
import { SystemDialog } from "../add-system"
import AlertButton from "../alerts/alert-button"
@@ -56,7 +59,18 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon, WebSocketIcon } from "../ui/icons"
import {
BatteryMediumIcon,
EthernetIcon,
GpuIcon,
HourglassIcon,
ThermometerIcon,
WebSocketIcon,
BatteryHighIcon,
BatteryLowIcon,
PlugChargingIcon,
BatteryFullIcon,
} from "../ui/icons"
const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500",
@@ -69,7 +83,7 @@ const STATUS_COLORS = {
* @param viewMode - "table" or "grid"
* @returns - Column definitions for the systems table
*/
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
return [
{
// size: 200,
@@ -134,7 +148,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.cpu,
accessorFn: ({ info }) => info.cpu || undefined,
id: "cpu",
name: () => t`CPU`,
cell: TableCellWithMeter,
@@ -143,7 +157,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
},
{
// accessorKey: "info.mp",
accessorFn: ({ info }) => info.mp,
accessorFn: ({ info }) => info.mp || undefined,
id: "memory",
name: () => t`Memory`,
cell: TableCellWithMeter,
@@ -151,7 +165,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.dp,
accessorFn: ({ info }) => info.dp || undefined,
id: "disk",
name: () => t`Disk`,
cell: DiskCellWithMultiple,
@@ -159,7 +173,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
header: sortableHeader,
},
{
accessorFn: ({ info }) => info.g,
accessorFn: ({ info }) => info.g || undefined,
id: "gpu",
name: () => "GPU",
cell: TableCellWithMeter,
@@ -172,9 +186,9 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
// TODO: remove this in future release in favor of la array
if (!sum) {
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0)
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) || undefined
}
return sum
return sum || undefined
},
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
size: 0,
@@ -217,7 +231,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
},
},
{
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024,
accessorFn: ({ info }) => info.bb || (info.b || 0) * 1024 * 1024 || undefined,
id: "net",
name: () => t`Net`,
size: 0,
@@ -229,7 +243,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
if (sys.status === SystemStatus.Paused) {
return null
}
const { value, unit } = formatBytes(info.getValue() as number, true, userSettings.unitNet, false)
const { value, unit } = formatBytes((info.getValue() || 0) as number, true, userSettings.unitNet, false)
return (
<span className="tabular-nums whitespace-nowrap">
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
@@ -259,11 +273,95 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
)
},
},
{
accessorFn: ({ info }) => info.bat?.[0],
id: "battery",
name: () => t({ message: "Bat", comment: "Battery label in systems table header" }),
size: 70,
Icon: BatteryMediumIcon,
header: sortableHeader,
hideSort: true,
cell(info) {
const [pct, state] = info.row.original.info.bat ?? []
if (pct === undefined) {
return null
}
const iconColor = pct < 10 ? "text-red-500" : pct < 25 ? "text-yellow-500" : "text-muted-foreground"
let Icon = PlugChargingIcon
if (state !== BatteryState.Charging) {
if (pct < 25) {
Icon = BatteryLowIcon
} else if (pct < 75) {
Icon = BatteryMediumIcon
} else if (pct < 95) {
Icon = BatteryHighIcon
} else {
Icon = BatteryFullIcon
}
}
const stateLabel =
state !== undefined ? (batteryStateTranslations[state as BatteryState]?.() ?? undefined) : undefined
return (
<Link
tabIndex={-1}
href={getPagePath($router, "system", { id: info.row.original.id })}
className="flex items-center gap-1 tabular-nums tracking-tight relative z-10"
title={stateLabel}
>
<Icon className={cn("size-3.5", iconColor)} />
<span className="min-w-10">{pct}%</span>
</Link>
)
},
},
{
accessorFn: ({ info }) => info.sv?.[0],
id: "services",
name: () => t`Services`,
size: 50,
Icon: TerminalSquareIcon,
header: sortableHeader,
hideSort: true,
sortingFn: (a, b) => {
// sort priorities: 1) failed services, 2) total services
const [totalCountA, numFailedA] = a.original.info.sv ?? [0, 0]
const [totalCountB, numFailedB] = b.original.info.sv ?? [0, 0]
if (numFailedA !== numFailedB) {
return numFailedA - numFailedB
}
return totalCountA - totalCountB
},
cell(info) {
const sys = info.row.original
const [totalCount, numFailed] = sys.info.sv ?? [0, 0]
if (sys.status !== SystemStatus.Up || totalCount === 0) {
return null
}
return (
<span className="tabular-nums whitespace-nowrap flex gap-1.5 items-center">
<span
className={cn("block size-2 rounded-full", {
[STATUS_COLORS[SystemStatus.Down]]: numFailed > 0,
[STATUS_COLORS[SystemStatus.Up]]: numFailed === 0,
})}
/>
{totalCount}{" "}
<span className="text-muted-foreground text-sm -ms-0.5">
({t`Failed`.toLowerCase()}: {numFailed})
</span>
</span>
)
},
},
{
accessorFn: ({ info }) => info.v,
id: "agent",
name: () => t`Agent`,
// invertSorting: true,
size: 50,
Icon: WifiIcon,
hideSort: true,
@@ -359,70 +457,92 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status, id } = info.row.original
const extraFs = Object.entries(sysInfo.efs ?? {})
// No extra disks - show basic meter
if (extraFs.length === 0) {
return TableCellWithMeter(info)
}
const rootDiskPct = sysInfo.dp
// sort extra disks by percentage descending
extraFs.sort((a, b) => b[1] - a[1])
function getMeterClass(pct: number) {
function getIndicatorColor(pct: number) {
const threshold = getMeterState(pct)
return cn(
"h-full",
return (
(status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)
}
function getMeterClass(pct: number) {
return cn("h-full", getIndicatorColor(pct))
}
// Extra disk indicators (max 3 dots - one per state if any disk exists in range)
const stateColors = [STATUS_COLORS.up, STATUS_COLORS.pending, STATUS_COLORS.down]
const extraDiskIndicators =
status !== SystemStatus.Up
? []
: [...new Set(extraFs.map(([, pct]) => getMeterState(pct)))].sort().map((state) => stateColors[state])
return (
<Tooltip>
<TooltipTrigger asChild>
<Link href={getPagePath($router, "system", { id })} tabIndex={-1} className="flex flex-col gap-0.5 w-full relative z-10">
<Link
href={getPagePath($router, "system", { id })}
tabIndex={-1}
className="flex flex-col gap-0.5 w-full relative z-10"
>
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8 shrink-0">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span className="flex-1 min-w-8 flex items-center gap-0.5 px-1 justify-end bg-muted h-[1em] rounded-sm overflow-hidden relative">
{/* Root disk */}
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
{/* Extra disks */}
{extraFs.map(([_name, pct], index) => (
<span key={index} className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
<span
className={cn("absolute inset-0", getMeterClass(rootDiskPct))}
style={{ width: `${rootDiskPct}%` }}
></span>
{/* Extra disk indicators */}
{extraDiskIndicators.map((color) => (
<span
key={color}
className={cn("size-1.5 rounded-full shrink-0 outline-[0.5px] outline-muted", color)}
/>
))}
</span>
</div>
</Link>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs pb-2">
<div className="grid gap-1.5">
<div className="grid gap-0.5">
<div className="text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums"><Trans context="Root disk label">Root</Trans></div>
<div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
</span>
</div>
<div className="grid gap-1">
<div className="grid gap-0.5">
<div className="text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums">
<Trans context="Root disk label">Root</Trans>
</div>
<div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
</span>
</div>
{extraFs.map(([name, pct]) => {
return (
<div key={name} className="grid gap-0.5">
<div className="text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate">{name}</div>
<div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
<span className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
</span>
</div>
</div>
)
})}
</div>
{extraFs.map(([name, pct]) => {
return (
<div key={name} className="grid gap-0.5">
<div className="text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate">
{name}
</div>
<div className="flex gap-2 items-center tabular-nums text-xs">
<span className="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
<span className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
</span>
</div>
</div>
)
})}
</div>
</TooltipContent>
</Tooltip>
)
@@ -537,5 +657,5 @@ export const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
</AlertDialog>
</>
)
}, [id, status, host, name, t, deleteOpen, editOpen])
}, [id, status, host, name, system, t, deleteOpen, editOpen])
})

View File

@@ -24,6 +24,7 @@ import {
LayoutGridIcon,
LayoutListIcon,
Settings2Icon,
XIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
@@ -47,7 +48,7 @@ import type { SystemRecord } from "@/types"
import AlertButton from "../alerts/alert-button"
import { $router, Link } from "../router"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import { SystemsTableColumns, ActionsButton, IndicatorDot } from "./systems-table-columns"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | SystemRecord["status"]
@@ -60,7 +61,7 @@ export default function SystemsTable() {
const upSystems = $upSystems.get()
const pausedSystems = $pausedSystems.get()
const { i18n, t } = useLingui()
const [filter, setFilter] = useState<string>()
const [filter, setFilter] = useState<string>("")
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [sorting, setSorting] = useBrowserStorage<SortingState>(
"sortMode",
@@ -145,7 +146,26 @@ export default function SystemsTable() {
</div>
<div className="flex gap-2 ms-auto w-full md:w-80">
<Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<div className="relative flex-1">
<Input
placeholder={t`Filter...`}
onChange={(e) => setFilter(e.target.value)}
value={filter}
className="ps-4 pe-10 w-full"
/>
{filter && (
<Button
type="button"
variant="ghost"
size="icon"
aria-label={t`Clear`}
className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground"
onClick={() => setFilter("")}
>
<XIcon className="h-4 w-4" />
</Button>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
@@ -278,6 +298,7 @@ export default function SystemsTable() {
upSystemsLength,
downSystemsLength,
pausedSystemsLength,
filter,
])
return (

View File

@@ -5,6 +5,7 @@ import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from "@/lib/utils"
import type { ChartData } from "@/types"
import { Separator } from "./separator"
import { AxisDomain } from "recharts/types/util/types"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
@@ -454,3 +455,15 @@ export {
xAxis,
// ChartStyle,
}
export function pinnedAxisDomain(): AxisDomain {
return [0, (dataMax: number) => {
if (dataMax > 10) {
return Math.round(dataMax)
}
if (dataMax > 1) {
return Math.round(dataMax / 0.1) * 0.1
}
return dataMax
}]
}

View File

@@ -131,6 +131,7 @@ export function HourglassIcon(props: SVGProps<SVGSVGElement>) {
)
}
// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
export function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 256 193" {...props} fill="currentColor">
@@ -139,3 +140,48 @@ export function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
export function BatteryMediumIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
<path d="M16 13H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>
)
}
// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
export function BatteryLowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
<path d="M16 20H8V6h8m.67-2H15V2H9v2H7.33C6.6 4 6 4.6 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34c.74 0 1.33-.59 1.33-1.33V5.33C18 4.6 17.4 4 16.67 4M15 16H9v3h6zm0-4.5H9v3h6z" />
</svg>
)
}
// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
export function BatteryHighIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
<path d="M16 9H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>
)
}
// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
export function BatteryFullIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
<path d="M16.67 4H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>
)
}
// https://github.com/phosphor-icons/core (MIT license)
export function PlugChargingIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 256 256" {...props} fill="currentColor">
<path d="M224,48H180V16a12,12,0,0,0-24,0V48H100V16a12,12,0,0,0-24,0V48H32.55C24.4,48,20,54.18,20,60A12,12,0,0,0,32,72H44v92a44.05,44.05,0,0,0,44,44h28v32a12,12,0,0,0,24,0V208h28a44.05,44.05,0,0,0,44-44V72h12a12,12,0,0,0,0-24ZM188,164a20,20,0,0,1-20,20H88a20,20,0,0,1-20-20V72H188Zm-85.86-29.17a12,12,0,0,1-1.38-11l12-32a12,12,0,1,1,22.48,8.42L129.32,116H144a12,12,0,0,1,11.24,16.21l-12,32a12,12,0,0,1-22.48-8.42L126.68,140H112A12,12,0,0,1,102.14,134.83Z" />
</svg>
)
}

View File

@@ -34,6 +34,7 @@
--table-header: hsl(225, 6%, 97%);
--chart-saturation: 65%;
--chart-lightness: 50%;
--container: 1500px;
}
.dark {
@@ -116,7 +117,6 @@
}
@layer utilities {
/* Fonts */
@supports (font-variation-settings: normal) {
:root {
@@ -141,6 +141,7 @@
body {
@apply bg-background text-foreground;
font-variant-ligatures: no-contextual;
}
button {
@@ -149,7 +150,8 @@
}
@utility container {
@apply max-w-370 mx-auto px-4;
max-width: var(--container);
@apply mx-auto px-4;
}
@utility link {
@@ -159,10 +161,6 @@
@utility ns-dialog {
/* New system dialog width */
min-width: 30.3rem;
:where(:lang(zh), :lang(zh-CN), :lang(ko)) & {
min-width: 27.9rem;
}
}
.recharts-tooltip-wrapper {
@@ -172,4 +170,4 @@
.recharts-yAxis {
@apply tabular-nums;
}
}

View File

@@ -1,10 +1,11 @@
import { t } from "@lingui/core/macro"
import { CpuIcon, HardDriveIcon, HourglassIcon, MemoryStickIcon, ServerIcon, ThermometerIcon } from "lucide-react"
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
import type { RecordSubscription } from "pocketbase"
import { EthernetIcon, GpuIcon } from "@/components/ui/icons"
import { $alerts } from "@/lib/stores"
import type { AlertInfo, AlertRecord } from "@/types"
import { pb } from "./api"
import { ThermometerIcon, BatteryMediumIcon, HourglassIcon } from "@/components/ui/icons"
/** Alert info for each alert type */
export const alertInfo: Record<string, AlertInfo> = {
@@ -83,6 +84,14 @@ export const alertInfo: Record<string, AlertInfo> = {
step: 0.1,
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
},
Battery: {
name: () => t`Battery`,
unit: "%",
icon: BatteryMediumIcon,
desc: () => t`Triggers when battery charge drops below a threshold`,
start: 20,
invert: true,
},
} as const
/** Helper to manage user alerts */

View File

@@ -94,11 +94,6 @@ export default [
label: "Português",
e: "🇧🇷",
},
{
lang: "tr",
label: "Türkçe",
e: "🇹🇷",
},
{
lang: "ru",
label: "Русский",
@@ -109,11 +104,21 @@ export default [
label: "Slovenščina",
e: "🇸🇮",
},
{
lang: "sr",
label: "Српски",
e: "🇷🇸",
},
{
lang: "sv",
label: "Svenska",
e: "🇸🇪",
},
{
lang: "tr",
label: "Türkçe",
e: "🇹🇷",
},
{
lang: "uk",
label: "Українська",

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-30 21:52\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \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"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 دقائق"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "إجراءات"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "نشط"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "الحالة النشطة"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "إضافة <0>نظام</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "إضافة {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "إضافة نظام جديد"
msgid "Add <0>System</0>"
msgstr "إضافة <0>نظام</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "إضافة رابط"
msgid "Adjust display options for charts."
msgstr "تعديل خيارات العرض للرسوم البيانية."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "تعديل عرض التخطيط الرئيسي"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "جميع الحاويات"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "يمكن الإيقاف"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "إلغاء"
@@ -316,6 +327,12 @@ msgstr "تحقق من السجلات لمزيد من التفاصيل."
msgid "Check your notification service"
msgstr "تحقق من خدمة الإشعارات الخاصة بك"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "مسح"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "انقر على حاوية لعرض مزيد من المعلومات."
@@ -438,6 +455,10 @@ msgstr "تفصيل وقت المعالج"
msgid "CPU Usage"
msgstr "استخدام وحدة المعالجة المركزية"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "إنشاء"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "إنشاء حساب"
@@ -469,15 +490,18 @@ msgstr "الحالة الحالية"
msgid "Cycles"
msgstr "الدورات"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "لوحة التحكم"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "يوميًا"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "الفترة الزمنية الافتراضية"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "حذف"
@@ -562,11 +586,16 @@ msgstr "تنزيل"
msgid "Duration"
msgstr "المدة"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "تعديل"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "إشعارات البريد الإشباكي"
msgid "Empty"
msgstr "فارغة"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "وقت النهاية"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "أدخل عنوان البريد الإشباكي لإعادة تعيين كلمة المرور"
@@ -598,6 +632,8 @@ msgstr "أدخل كلمة المرور لمرة واحدة الخاصة بك."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
msgid "Fahrenheit (°F)"
msgstr "فهرنهايت (°ف)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "فشل"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "السمات الفاشلة:"
@@ -648,6 +688,7 @@ msgstr "فشل في المصادقة"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "فشل في حفظ الإعدادات"
@@ -706,6 +747,10 @@ msgstr "ممتلئة"
msgid "General"
msgstr "عام"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "عالمي"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "محركات GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "صورة"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "غير نشط"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "عنوان البريد الإشباكي غير صالح."
@@ -767,6 +816,10 @@ msgstr "اللغة"
msgid "Layout"
msgstr "التخطيط"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "عرض التخطيط"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "دورة الحياة"
@@ -947,12 +1000,19 @@ msgstr "دعم OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "مرة واحدة"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "كلمة مرور لمرة واحدة"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "فتح القائمة"
@@ -969,6 +1029,7 @@ msgstr "أخرى"
msgid "Overwrite existing alerts"
msgstr "الكتابة فوق التنبيهات الحالية"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "يجب أن تكون كلمة المرور أقل من 72 بايت."
msgid "Password reset request received"
msgstr "تم استلام طلب إعادة تعيين كلمة المرور"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "الماضي"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "إيقاف مؤقت"
@@ -1082,6 +1147,10 @@ msgstr "تم بدء العملية"
msgid "Public Key"
msgstr "المفتاح العام"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "ساعات الهدوء"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "تم الاستلام"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "تحديث"
@@ -1173,6 +1243,18 @@ msgstr "حفظ الإعدادات"
msgid "Save system"
msgstr "احفظ النظام"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "جدولة"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "جدولة ساعات الهدوء حيث لن يتم إرسال الإشعارات، مثل أثناء فترات الصيانة."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "جدولة ساعات الهدوء حيث لن يتم إرسال الإشعارات."
#: src/components/navbar.tsx
msgid "Search"
msgstr "بحث"
@@ -1185,6 +1267,10 @@ msgstr "البحث عن الأنظمة أو الإعدادات..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "راجع <0>إعدادات الإشعارات</0> لتكوين كيفية تلقي التنبيهات."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "تحديد {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "تم الإرسال"
@@ -1197,6 +1283,10 @@ msgstr "الرقم التسلسلي"
msgid "Service Details"
msgstr "تفاصيل الخدمة"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "الخدمات"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "تعيين عتبات النسبة المئوية لألوان العداد."
@@ -1224,8 +1314,14 @@ msgstr "إعدادات SMTP"
msgid "Sort By"
msgstr "الترتيب حسب"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "وقت البدء"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "الحالة"
@@ -1250,9 +1346,15 @@ msgstr "مساحة التبديل المستخدمة من قبل النظام"
msgid "Swap Usage"
msgstr "استخدام التبديل"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "يتم التفعيل عندما يتغير الحالة بين التش
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "النوع"
@@ -1469,7 +1573,12 @@ msgstr "قيد التشغيل"
msgid "Up ({upSystemsLength})"
msgstr "قيد التشغيل ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "تحديث"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "تم التحديث"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-20 21:37\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 минути"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Действия"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Активен"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Активно състояние"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Добави <0>Система</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Добави {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Добави нова система"
msgid "Add <0>System</0>"
msgstr "Добави <0>Система</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Добави URL"
msgid "Adjust display options for charts."
msgstr "Настрой опциите за показване на диаграмите."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Настройка ширината на основния макет"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Всички контейнери"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Може да се спре"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Откажи"
@@ -316,6 +327,12 @@ msgstr "Провери log-овете за повече информация."
msgid "Check your notification service"
msgstr "Провери услугата си за удостоверяване"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Изчисти"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Кликнете върху контейнер, за да видите повече информация."
@@ -438,6 +455,10 @@ msgstr "Разбивка на времето на CPU"
msgid "CPU Usage"
msgstr "Употреба на процесор"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Създай"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Създай акаунт"
@@ -469,15 +490,18 @@ msgstr "Текущо състояние"
msgid "Cycles"
msgstr "Цикли"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Табло"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Дневно"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Времеви диапазон по подразбиране"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Изтрий"
@@ -562,11 +586,16 @@ msgstr "Изтегляне"
msgid "Duration"
msgstr "Продължителност"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Редактирай"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Имейл нотификации"
msgid "Empty"
msgstr "Празна"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Крайно време"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Въведи имейл адрес за да нулираш паролата"
@@ -598,6 +632,8 @@ msgstr "Въведете Вашата еднократна парола."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Експортирай конфигурацията на системи
msgid "Fahrenheit (°F)"
msgstr "Фаренхайт (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Неуспешно"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Неуспешни атрибути:"
@@ -648,6 +688,7 @@ msgstr "Неуспешно удостоверяване"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Неуспешно запазване на настройки"
@@ -706,6 +747,10 @@ msgstr "Пълна"
msgid "General"
msgstr "Общо"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Глобален"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU двигатели"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Образ"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Неактивен"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Невалиден имейл адрес."
@@ -767,6 +816,10 @@ msgstr "Език"
msgid "Layout"
msgstr "Подреждане"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Ширина на макета"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Жизнен цикъл"
@@ -947,12 +1000,19 @@ msgstr "Поддръжка на OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Еднократен"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Еднократна парола"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Отвори менюто"
@@ -969,6 +1029,7 @@ msgstr "Други"
msgid "Overwrite existing alerts"
msgstr "Презапиши съществуващи тревоги"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Паролата трябва да е по-малка от 72 байта
msgid "Password reset request received"
msgstr "Получено е искането за нулиране на паролата"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Минал"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Пауза"
@@ -1082,6 +1147,10 @@ msgstr "Процесът стартира"
msgid "Public Key"
msgstr "Публичен ключ"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Тихи часове"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Получени"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Опресни"
@@ -1173,6 +1243,18 @@ msgstr "Запази настройките"
msgid "Save system"
msgstr "Запази система"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "График"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Планирай тихи часове, когато няма да се изпращат известия, като например по време на периоди на поддръжка."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Планирай тихи часове, когато няма да се изпращат известия."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Търси"
@@ -1185,6 +1267,10 @@ msgstr "Търси за системи или настройки..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Виж <0>настройките за нотификациите</0> за да конфигурираш как получаваш тревоги."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Избери {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Изпратени"
@@ -1197,6 +1283,10 @@ msgstr "Сериен номер"
msgid "Service Details"
msgstr "Детайли на услугата"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Услуги"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Задайте процентни прагове за цветовете на измервателните уреди."
@@ -1224,8 +1314,14 @@ msgstr "Настройки за SMTP"
msgid "Sort By"
msgstr "Сортиране по"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Начален час"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Състояние"
@@ -1250,9 +1346,15 @@ msgstr "Изполван swap от системата"
msgid "Swap Usage"
msgstr "Използване на swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1417,7 +1519,7 @@ msgstr "Задейства се, когато употребата на проц
#: src/lib/alerts.ts
msgid "Triggers when GPU usage exceeds a threshold"
msgstr ""
msgstr "Задейства се, когато използването на GPU надвиши праг"
#: src/lib/alerts.ts
msgid "Triggers when memory usage exceeds a threshold"
@@ -1431,6 +1533,8 @@ msgstr "Задейства се, когато статуса превключв
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Задейства се, когато употребата на някой диск надивши зададен праг"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Тип"
@@ -1469,7 +1573,12 @@ msgstr "Нагоре"
msgid "Up ({upSystemsLength})"
msgstr "Нагоре ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Актуализирай"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Актуализирано"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -43,7 +43,7 @@ msgstr "1 hodina"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "1 min"
msgstr ""
msgstr "1 min"
#: src/lib/utils.ts
msgid "1 minute"
@@ -60,7 +60,7 @@ msgstr "12 hodin"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "15 min"
msgstr ""
msgstr "15 min"
#: src/lib/utils.ts
msgid "24 hours"
@@ -73,16 +73,19 @@ msgstr "30 dní"
#. Load average
#: src/components/charts/load-average-chart.tsx
msgid "5 min"
msgstr ""
msgstr "5 min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Akce"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktivní"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Aktivní stav"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Přidat <0>Systém</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Přidat {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Přidat nový systém"
msgid "Add <0>System</0>"
msgstr "Přidat <0>Systém</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Přidat URL"
msgid "Adjust display options for charts."
msgstr "Upravit možnosti zobrazení pro grafy."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Upravit šířku hlavního rozvržení"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Všechny kontejnery"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Může zastavit"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Zrušit"
@@ -316,6 +327,12 @@ msgstr "Pro více informací zkontrolujte logy."
msgid "Check your notification service"
msgstr "Zkontrolujte službu upozornění"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Vymazat"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Klikněte na kontejner pro zobrazení dalších informací."
@@ -438,6 +455,10 @@ msgstr "Rozdělení času CPU"
msgid "CPU Usage"
msgstr "Využití procesoru"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Vytvořit"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Vytvořit účet"
@@ -469,15 +490,18 @@ msgstr "Aktuální stav"
msgid "Cycles"
msgstr "Cykly"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Přehled"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Denně"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Výchozí doba"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Odstranit"
@@ -505,11 +529,11 @@ msgstr "Vybíjení"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Disk"
msgstr "Disk"
msgstr ""
#: src/components/routes/system.tsx
msgid "Disk I/O"
msgstr "Disk I/O"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Disk unit"
@@ -562,11 +586,16 @@ msgstr "Stažení"
msgid "Duration"
msgstr "Doba trvání"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Upravit"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Upravit {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Emailová upozornění"
msgid "Empty"
msgstr "Prázdná"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Čas ukončení"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Zadejte e-mailovou adresu pro obnovu hesla"
@@ -598,6 +632,8 @@ msgstr "Zadejte Vaše jednorázové heslo."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Exportovat aktuální konfiguraci systémů."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheita (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Selhalo"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Neúspěšné atributy:"
@@ -648,6 +688,7 @@ msgstr "Ověření se nezdařilo"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Nepodařilo se uložit nastavení"
@@ -679,7 +720,7 @@ msgstr "Otisk"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr "Firmware"
msgstr ""
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -706,6 +747,10 @@ msgstr "Plná"
msgid "General"
msgstr "Obecné"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Globální"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU enginy"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Obraz"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Neaktivní"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Neplatná e-mailová adresa."
@@ -767,6 +816,10 @@ msgstr "Jazyk"
msgid "Layout"
msgstr "Rozvržení"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Šířka rozvržení"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Životní cyklus"
@@ -774,7 +827,7 @@ msgstr "Životní cyklus"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "limit"
msgstr "limit"
msgstr ""
#: src/components/routes/system.tsx
msgid "Load Average"
@@ -873,7 +926,7 @@ msgstr "Využití paměti docker kontejnerů"
#: src/components/routes/system/smart-table.tsx
msgid "Model"
msgstr "Model"
msgstr ""
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
@@ -947,12 +1000,19 @@ msgstr "Podpora OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Jednorázové"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Jednorázové heslo"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Otevřít menu"
@@ -969,6 +1029,7 @@ msgstr "Jiné"
msgid "Overwrite existing alerts"
msgstr "Přepsat existující upozornění"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Heslo musí být menší než 72 bytů."
msgid "Password reset request received"
msgstr "Žádost o obnovu hesla byla přijata"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Minulé"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pozastavit"
@@ -1057,7 +1122,7 @@ msgstr "Přihlaste se prosím k vašemu účtu"
#: src/components/add-system.tsx
msgid "Port"
msgstr "Port"
msgstr ""
#. Power On Time
#: src/components/routes/system/smart-table.tsx
@@ -1082,6 +1147,10 @@ msgstr "Proces spuštěn"
msgid "Public Key"
msgstr "Veřejný klíč"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Tiché hodiny"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Přijato"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Aktualizovat"
@@ -1173,6 +1243,18 @@ msgstr "Uložit nastavení"
msgid "Save system"
msgstr "Uložit systém"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Plán"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Naplánujte tiché hodiny, kdy se nebudou odesílat oznámení, například během období údržby."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Naplánujte tiché hodiny, kdy se nebudou odesílat oznámení."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Hledat"
@@ -1185,6 +1267,10 @@ msgstr "Hledat systémy nebo nastavení..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak přijímáte upozornění."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Vybrat {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Odeslat"
@@ -1197,6 +1283,10 @@ msgstr "Sériové číslo"
msgid "Service Details"
msgstr "Detaily služby"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Služby"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Nastavte procentuální prahové hodnoty pro barvy měřičů."
@@ -1224,8 +1314,14 @@ msgstr "Nastavení SMTP"
msgid "Sort By"
msgstr "Seřadit podle"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Čas začátku"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Stav"
@@ -1250,9 +1346,15 @@ msgstr "Swap prostor využívaný systémem"
msgid "Swap Usage"
msgstr "Swap využití"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1349,7 +1451,7 @@ msgstr "Přepnout motiv"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Token"
msgstr "Token"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1431,6 +1533,8 @@ msgstr "Spouští se, když se změní dostupnost"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Typ"
@@ -1469,7 +1573,12 @@ msgstr "Funkční"
msgid "Up ({upSystemsLength})"
msgstr "Funkční ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Aktualizovat"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Aktualizováno"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 minutter"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Handlinger"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktiv"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Aktiv tilstand"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Tilføj <0>System</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Tilføj {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Tilføj nyt system"
msgid "Add <0>System</0>"
msgstr "Tilføj <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Tilføj URL"
msgid "Adjust display options for charts."
msgstr "Juster visningsindstillinger for diagrammer."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Juster bredden af hovedlayoutet"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Alle containere"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,13 +273,14 @@ msgid "Can stop"
msgstr "Kan stoppe"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Fortryd"
#: src/components/systemd-table/systemd-table.tsx
msgid "Capabilities"
msgstr ""
msgstr "Funktioner"
#: src/components/routes/system/smart-table.tsx
msgid "Capacity"
@@ -316,6 +327,12 @@ msgstr "Tjek logfiler for flere detaljer."
msgid "Check your notification service"
msgstr "Tjek din notifikationstjeneste"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Ryd"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Klik på en container for at se mere information."
@@ -348,7 +365,7 @@ msgstr "Bekræft adgangskode"
#: src/components/systemd-table/systemd-table.tsx
msgid "Conflicts"
msgstr ""
msgstr "Konflikter"
#: src/components/active-alerts.tsx
msgid "Connection is down"
@@ -421,7 +438,7 @@ msgstr "CPU-kerner"
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "CPU Peak"
msgstr ""
msgstr "CPU Peak"
#: src/components/systemd-table/systemd-table.tsx
msgid "CPU time"
@@ -438,6 +455,10 @@ msgstr "CPU-tidsfordeling"
msgid "CPU Usage"
msgstr "CPU forbrug"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Opret"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Opret konto"
@@ -469,15 +490,18 @@ msgstr "Nuværende tilstand"
msgid "Cycles"
msgstr "Cykler"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Oversigtspanel"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Dagligt"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Standard tidsperiode"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Slet"
@@ -562,11 +586,16 @@ msgstr "Hent ned"
msgid "Duration"
msgstr "Varighed"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Rediger"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Rediger {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Email-notifikationer"
msgid "Empty"
msgstr "Tom"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Sluttid"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Indtast e-mailadresse for at nulstille adgangskoden"
@@ -598,6 +632,8 @@ msgstr "Indtast din engangsadgangskode."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Eksporter din nuværende systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Mislykkedes"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Mislykkede attributter:"
@@ -648,6 +688,7 @@ msgstr "Kunne ikke godkende"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Kunne ikke gemme indstillinger"
@@ -679,7 +720,7 @@ msgstr "Fingeraftryk"
#: src/components/routes/system/smart-table.tsx
msgid "Firmware"
msgstr "Firmware"
msgstr ""
#: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -706,6 +747,10 @@ msgstr "Fuldt opladt"
msgid "General"
msgstr "Generelt"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-enheder"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Billede"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inaktiv"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Ugyldig email adresse."
@@ -767,6 +816,10 @@ msgstr "Sprog"
msgid "Layout"
msgstr "Opstilling"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Layoutbredde"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Livscyklus"
@@ -947,12 +1000,19 @@ msgstr "OAuth 2 / OIDC understøttelse"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Engangs"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Engangsadgangskode"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Åbn menu"
@@ -969,6 +1029,7 @@ msgstr "Andre"
msgid "Overwrite existing alerts"
msgstr "Overskriv eksisterende alarmer"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Adgangskoden skal være mindre end 72 bytes."
msgid "Password reset request received"
msgstr "Anmodning om nulstilling af adgangskode modtaget"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Tidligere"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pause"
@@ -1082,6 +1147,10 @@ msgstr "Proces startet"
msgid "Public Key"
msgstr "Offentlig nøgle"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Stille timer"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Modtaget"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Opdater"
@@ -1173,6 +1243,18 @@ msgstr "Gem indstillinger"
msgid "Save system"
msgstr "Gem system"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Planlæg"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Planlæg stille timer hvor meddelelser ikke vil blive sendt, såsom under vedligeholdelsesperioder."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Planlæg stille timer hvor meddelelser ikke vil blive sendt."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Søg"
@@ -1185,6 +1267,10 @@ msgstr "Søg efter systemer eller indstillinger..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtager alarmer."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Vælg {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Sendt"
@@ -1197,6 +1283,10 @@ msgstr "Serienummer"
msgid "Service Details"
msgstr "Tjenestedetaljer"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Tjenester"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Indstil procentvise tærskler for målerfarver."
@@ -1224,8 +1314,14 @@ msgstr "SMTP-indstillinger"
msgid "Sort By"
msgstr "Sorter efter"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Starttid"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Tilstand"
@@ -1250,9 +1346,15 @@ msgstr "Swap plads brugt af systemet"
msgid "Swap Usage"
msgstr "Swap forbrug"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1417,7 +1519,7 @@ msgstr "Udløser når CPU-forbrug overstiger en tærskel"
#: src/lib/alerts.ts
msgid "Triggers when GPU usage exceeds a threshold"
msgstr ""
msgstr "Udløses når GPU-brug overstiger en grænse"
#: src/lib/alerts.ts
msgid "Triggers when memory usage exceeds a threshold"
@@ -1431,6 +1533,8 @@ msgstr "Udløser når status skifter mellem op og ned"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Udløser når brugen af en disk overstiger en tærskel"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Type"
@@ -1469,7 +1573,12 @@ msgstr "Oppe"
msgid "Up ({upSystemsLength})"
msgstr "Oppe ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Opdater"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Opdateret"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 22:59\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 Min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Aktionen"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktiv"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Aktiver Zustand"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>System</0> hinzufügen"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "{foo} hinzufügen"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Neues System hinzufügen"
msgid "Add <0>System</0>"
msgstr "<0>System</0> hinzufügen"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "URL hinzufügen"
msgid "Adjust display options for charts."
msgstr "Anzeigeoptionen für Diagramme anpassen."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Breite des Hauptlayouts anpassen"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Alle Container"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Kann stoppen"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Abbrechen"
@@ -316,6 +327,12 @@ msgstr "Überprüfe die Protokolle für weitere Details."
msgid "Check your notification service"
msgstr "Überprüfe deinen Benachrichtigungsdienst"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Löschen"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Klicke auf einen Container, um weitere Informationen zu sehen."
@@ -438,6 +455,10 @@ msgstr "CPU-Zeit-Aufschlüsselung"
msgid "CPU Usage"
msgstr "CPU-Auslastung"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Erstellen"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Konto erstellen"
@@ -469,15 +490,18 @@ msgstr "Aktueller Zustand"
msgid "Cycles"
msgstr "Zyklen"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Dashboard"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Täglich"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Standardzeitraum"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Löschen"
@@ -562,11 +586,16 @@ msgstr "Herunterladen"
msgid "Duration"
msgstr "Dauer"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Bearbeiten"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "{foo} bearbeiten"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "E-Mail-Benachrichtigungen"
msgid "Empty"
msgstr "Leer"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Endzeit"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "E-Mail-Adresse eingeben, um das Passwort zurückzusetzen"
@@ -598,6 +632,8 @@ msgstr "Geben Sie Ihr Einmalpasswort ein."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Fehlgeschlagen"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Fehlgeschlagene Attribute:"
@@ -648,6 +688,7 @@ msgstr "Authentifizierung fehlgeschlagen"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Einstellungen konnten nicht gespeichert werden"
@@ -706,6 +747,10 @@ msgstr "Voll"
msgid "General"
msgstr "Allgemein"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-Engines"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Image"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inaktiv"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Ungültige E-Mail-Adresse."
@@ -767,6 +816,10 @@ msgstr "Sprache"
msgid "Layout"
msgstr "Anordnung"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Layoutbreite"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Lebenszyklus"
@@ -947,12 +1000,19 @@ msgstr "OAuth 2 / OIDC-Unterstützung"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Einmalig"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Einmalpasswort"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Menü öffnen"
@@ -969,6 +1029,7 @@ msgstr "Andere"
msgid "Overwrite existing alerts"
msgstr "Bestehende Warnungen überschreiben"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Das Passwort muss weniger als 72 Bytes lang sein."
msgid "Password reset request received"
msgstr "Anfrage zum Zurücksetzen des Passworts erhalten"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr ""
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pause"
@@ -1082,6 +1147,10 @@ msgstr "Prozess gestartet"
msgid "Public Key"
msgstr "Öffentlicher Schlüssel"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Ruhezeiten"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Empfangen"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Aktualisieren"
@@ -1138,7 +1208,7 @@ msgstr "Fortsetzen"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr "Root"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1173,6 +1243,18 @@ msgstr "Einstellungen speichern"
msgid "Save system"
msgstr "System speichern"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Zeitplan"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Plane Ruhezeiten, in denen keine Benachrichtigungen gesendet werden, z. B. während Wartungszeiten."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Plane Ruhezeiten, in denen keine Benachrichtigungen gesendet werden."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Suche"
@@ -1185,6 +1267,10 @@ msgstr "Nach Systemen oder Einstellungen suchen..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du Warnungen erhältst."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Auswählen {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Gesendet"
@@ -1197,6 +1283,10 @@ msgstr "Seriennummer"
msgid "Service Details"
msgstr "Servicedetails"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Dienste"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Prozentuale Schwellenwerte für Zählerfarben festlegen."
@@ -1224,8 +1314,14 @@ msgstr "SMTP-Einstellungen"
msgid "Sort By"
msgstr "Sortieren nach"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Startzeit"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Status"
@@ -1250,9 +1346,15 @@ msgstr "Vom System genutzter Swap-Speicher"
msgid "Swap Usage"
msgstr "Swap-Nutzung"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Löst aus, wenn der Status zwischen online und offline wechselt"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Typ"
@@ -1469,7 +1573,12 @@ msgstr "aktiv"
msgid "Up ({upSystemsLength})"
msgstr "aktiv ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Aktualisieren"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Aktualisiert"

File diff suppressed because it is too large Load Diff

View File

@@ -71,13 +71,16 @@ msgid "5 min"
msgstr "5 min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Actions"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Active"
@@ -90,12 +93,14 @@ msgid "Active state"
msgstr "Active state"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Add <0>System</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Add {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Add New System"
msgid "Add <0>System</0>"
msgstr "Add <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -109,6 +114,10 @@ msgstr "Add URL"
msgid "Adjust display options for charts."
msgstr "Adjust display options for charts."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Adjust the width of the main layout"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -142,6 +151,7 @@ msgstr "All Containers"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -258,6 +268,7 @@ msgid "Can stop"
msgstr "Can stop"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Cancel"
@@ -311,6 +322,12 @@ msgstr "Check logs for more details."
msgid "Check your notification service"
msgstr "Check your notification service"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Clear"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Click on a container to view more information."
@@ -433,6 +450,10 @@ msgstr "CPU Time Breakdown"
msgid "CPU Usage"
msgstr "CPU Usage"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Create"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Create account"
@@ -464,15 +485,18 @@ msgstr "Current state"
msgid "Cycles"
msgstr "Cycles"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Dashboard"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Daily"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Default time period"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Delete"
@@ -557,11 +581,16 @@ msgstr "Download"
msgid "Duration"
msgstr "Duration"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Edit"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Edit {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -577,6 +606,11 @@ msgstr "Email notifications"
msgid "Empty"
msgstr "Empty"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "End Time"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Enter email address to reset password"
@@ -593,6 +627,8 @@ msgstr "Enter your one-time password."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -633,6 +669,10 @@ msgstr "Export your current systems configuration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Failed"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Failed Attributes:"
@@ -643,6 +683,7 @@ msgstr "Failed to authenticate"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Failed to save settings"
@@ -701,6 +742,10 @@ msgstr "Full"
msgid "General"
msgstr "General"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU Engines"
@@ -745,6 +790,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Image"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inactive"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Invalid email address."
@@ -762,6 +811,10 @@ msgstr "Language"
msgid "Layout"
msgstr "Layout"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Layout width"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Lifecycle"
@@ -942,12 +995,19 @@ msgstr "OAuth 2 / OIDC support"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "On each restart, systems in the database will be updated to match the systems defined in the file."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "One-time"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "One-time password"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Open menu"
@@ -964,6 +1024,7 @@ msgstr "Other"
msgid "Overwrite existing alerts"
msgstr "Overwrite existing alerts"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -996,6 +1057,10 @@ msgstr "Password must be less than 72 bytes."
msgid "Password reset request received"
msgstr "Password reset request received"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Past"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pause"
@@ -1077,6 +1142,10 @@ msgstr "Process started"
msgid "Public Key"
msgstr "Public Key"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Quiet Hours"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1089,6 +1158,7 @@ msgstr "Received"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Refresh"
@@ -1168,6 +1238,18 @@ msgstr "Save Settings"
msgid "Save system"
msgstr "Save system"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Schedule"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Schedule quiet hours where notifications will not be sent."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Search"
@@ -1180,6 +1262,10 @@ msgstr "Search for systems or settings..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "See <0>notification settings</0> to configure how you receive alerts."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Select {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Sent"
@@ -1192,6 +1278,10 @@ msgstr "Serial Number"
msgid "Service Details"
msgstr "Service Details"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Services"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Set percentage thresholds for meter colors."
@@ -1219,8 +1309,14 @@ msgstr "SMTP settings"
msgid "Sort By"
msgstr "Sort By"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Start Time"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "State"
@@ -1245,9 +1341,15 @@ msgstr "Swap space used by the system"
msgid "Swap Usage"
msgstr "Swap Usage"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1426,6 +1528,8 @@ msgstr "Triggers when status switches between up and down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Triggers when usage of any disk exceeds a threshold"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Type"
@@ -1464,7 +1568,12 @@ msgstr "Up"
msgid "Up ({upSystemsLength})"
msgstr "Up ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Update"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Updated"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-11-04 22:13\n"
"PO-Revision-Date: 2025-12-01 23:32\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Acciones"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Activo"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Estado activo"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Agregar <0>sistema</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Agregar {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Agregar nuevo sistema"
msgid "Add <0>System</0>"
msgstr "Agregar <0>sistema</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Agregar URL"
msgid "Adjust display options for charts."
msgstr "Ajustar las opciones de visualización para los gráficos."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Ajustar el ancho del diseño principal"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Todos los contenedores"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -252,17 +262,18 @@ msgstr "Caché / Buffers"
#: src/components/systemd-table/systemd-table.tsx
msgid "Can reload"
msgstr "Puede recargar"
msgstr "Puede recargarse"
#: src/components/systemd-table/systemd-table.tsx
msgid "Can start"
msgstr "Puede iniciar"
msgstr "Puede iniciarse"
#: src/components/systemd-table/systemd-table.tsx
msgid "Can stop"
msgstr "Puede detener"
msgstr "Puede detenerse"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Cancelar"
@@ -316,9 +327,15 @@ msgstr "Revisa los registros para más detalles."
msgid "Check your notification service"
msgstr "Verifica tu servicio de notificaciones"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Haga clic en un contenedor para ver más información."
msgstr "Haz clic en un contenedor para ver más información."
#: src/components/routes/system/smart-table.tsx
msgid "Click on a device to view more information."
@@ -438,6 +455,10 @@ msgstr "Desglose de tiempo de CPU"
msgid "CPU Usage"
msgstr "Uso de CPU"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Crear"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Crear cuenta"
@@ -469,15 +490,18 @@ msgstr "Estado actual"
msgid "Cycles"
msgstr "Ciclos"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Tablero"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Diariamente"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Periodo de tiempo predeterminado"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Eliminar"
@@ -562,11 +586,16 @@ msgstr "Descargar"
msgid "Duration"
msgstr "Duración"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Editar"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Editar {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Notificaciones por correo"
msgid "Empty"
msgstr "Vacía"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Hora de finalización"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Ingresa la dirección de correo electrónico para restablecer la contraseña"
@@ -598,6 +632,8 @@ msgstr "Ingrese su contraseña de un solo uso."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Exporta la configuración actual de sus sistemas."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Fallido"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Atributos fallidos:"
@@ -648,6 +688,7 @@ msgstr "Error al autenticar"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Error al guardar la configuración"
@@ -706,6 +747,10 @@ msgstr "Llena"
msgid "General"
msgstr "General"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Motores GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Imagen"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inactivo"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Dirección de correo electrónico no válida."
@@ -767,6 +816,10 @@ msgstr "Idioma"
msgid "Layout"
msgstr "Diseño"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Ancho del diseño"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Ciclo de vida"
@@ -947,12 +1000,19 @@ msgstr "Soporte para OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "En cada reinicio, los sistemas en la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Una vez"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Contraseña de un solo uso"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Abrir menú"
@@ -969,6 +1029,7 @@ msgstr "Otro"
msgid "Overwrite existing alerts"
msgstr "Sobrescribir alertas existentes"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "La contraseña debe ser menor de 72 bytes."
msgid "Password reset request received"
msgstr "Solicitud de restablecimiento de contraseña recibida"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Pasado"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pausar"
@@ -1082,6 +1147,10 @@ msgstr "Proceso iniciado"
msgid "Public Key"
msgstr "Clave pública"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Horas de silencio"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Recibido"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Actualizar"
@@ -1173,6 +1243,18 @@ msgstr "Guardar configuración"
msgid "Save system"
msgstr "Guardar sistema"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Programar"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Programe horas de silencio donde no se enviarán notificaciones, como durante períodos de mantenimiento."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Programe horas de silencio donde no se enviarán notificaciones."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Buscar"
@@ -1185,6 +1267,10 @@ msgstr "Buscar sistemas o configuraciones..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Consulta <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Seleccionar {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Enviado"
@@ -1197,6 +1283,10 @@ msgstr "Número de serie"
msgid "Service Details"
msgstr "Detalles del servicio"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Servicios"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Establecer umbrales de porcentaje para los colores de los medidores."
@@ -1224,8 +1314,14 @@ msgstr "Configuración SMTP"
msgid "Sort By"
msgstr "Ordenar por"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Hora de inicio"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Estado"
@@ -1250,9 +1346,15 @@ msgstr "Espacio de swap utilizado por el sistema"
msgid "Swap Usage"
msgstr "Uso de swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Se activa cuando el estado cambia entre activo e inactivo"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Tipo"
@@ -1469,7 +1573,12 @@ msgstr "Activo"
msgid "Up ({upSystemsLength})"
msgstr "Activo ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Actualizar"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Actualizado"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "۵ دقیقه"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "عملیات"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "فعال"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "وضعیت فعال"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "افزودن <0>سیستم</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "افزودن {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "افزودن سیستم جدید"
msgid "Add <0>System</0>"
msgstr "افزودن <0>سیستم</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "افزودن آدرس اینترنتی"
msgid "Adjust display options for charts."
msgstr "تنظیم گزینه‌های نمایش برای نمودارها."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "تنظیم عرض چیدمان اصلی"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "همه کانتینرها"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "می‌تواند متوقف شود"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "لغو"
@@ -316,6 +327,12 @@ msgstr "برای جزئیات بیشتر، لاگ‌ها را بررسی کنی
msgid "Check your notification service"
msgstr "سرویس اطلاع‌رسانی خود را بررسی کنید"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "برای مشاهده اطلاعات بیشتر روی کانتینر کلیک کنید."
@@ -438,6 +455,10 @@ msgstr "تجزیه زمان CPU"
msgid "CPU Usage"
msgstr "میزان استفاده از پردازنده"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "ایجاد"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "ایجاد حساب کاربری"
@@ -469,15 +490,18 @@ msgstr "وضعیت فعلی"
msgid "Cycles"
msgstr "چرخه‌ها"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "داشبورد"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "روزانه"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "بازه زمانی پیش‌فرض"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "حذف"
@@ -562,11 +586,16 @@ msgstr "دانلود"
msgid "Duration"
msgstr "مدت زمان"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "ویرایش"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "ویرایش {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "اعلان‌های ایمیلی"
msgid "Empty"
msgstr "خالی"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "زمان پایان"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "آدرس ایمیل را برای بازنشانی رمز عبور وارد کنید"
@@ -598,6 +632,8 @@ msgstr "رمز عبور یک‌بار مصرف خود را وارد کنید."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "پیکربندی سیستم‌های فعلی خود را خارج کن
msgid "Fahrenheit (°F)"
msgstr "فارنهایت (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "ناموفق"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "ویژگی‌های ناموفق:"
@@ -648,6 +688,7 @@ msgstr "احراز هویت ناموفق بود"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "ذخیره تنظیمات ناموفق بود"
@@ -706,6 +747,10 @@ msgstr "پر"
msgid "General"
msgstr "عمومی"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "جهانی"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "موتورهای GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "تصویر"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "غیرفعال"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "آدرس ایمیل نامعتبر است."
@@ -767,6 +816,10 @@ msgstr "زبان"
msgid "Layout"
msgstr "طرح‌بندی"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "عرض چیدمان"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "چرخه حیات"
@@ -947,12 +1000,19 @@ msgstr "پشتیبانی از OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "در هر بار راه‌اندازی مجدد، سیستم‌های موجود در پایگاه داده با سیستم‌های تعریف شده در فایل مطابقت داده می‌شوند."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "یک‌بار مصرف"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "رمز عبور یک‌بار مصرف"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "باز کردن منو"
@@ -969,6 +1029,7 @@ msgstr "سایر"
msgid "Overwrite existing alerts"
msgstr "بازنویسی هشدارهای موجود"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "رمز عبور باید کمتر از ۷۲ بایت باشد."
msgid "Password reset request received"
msgstr "درخواست بازنشانی رمز عبور دریافت شد"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "گذشته"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "توقف"
@@ -1082,6 +1147,10 @@ msgstr "فرآیند شروع شد"
msgid "Public Key"
msgstr "کلید عمومی"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "ساعات آرام"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "دریافت شد"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "تازه‌سازی"
@@ -1173,6 +1243,18 @@ msgstr "ذخیره تنظیمات"
msgid "Save system"
msgstr "ذخیره سیستم"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "برنامه‌ریزی"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "برنامه‌ریزی ساعات آرام که در آن اعلان‌ها ارسال نخواهند شد، مانند در طول دوره‌های تعمیر و نگهداری."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "برنامه‌ریزی ساعات آرام که در آن اعلان‌ها ارسال نخواهند شد."
#: src/components/navbar.tsx
msgid "Search"
msgstr "جستجو"
@@ -1185,6 +1267,10 @@ msgstr "جستجو برای سیستم‌ها یا تنظیمات..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "برای پیکربندی نحوه دریافت هشدارها، به <0>تنظیمات اعلان</0> مراجعه کنید."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "انتخاب {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "ارسال شد"
@@ -1197,6 +1283,10 @@ msgstr "شماره سریال"
msgid "Service Details"
msgstr "جزئیات سرویس"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "سرویس‌ها"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "آستانه های درصدی را برای رنگ های متر تنظیم کنید."
@@ -1224,8 +1314,14 @@ msgstr "تنظیمات SMTP"
msgid "Sort By"
msgstr "مرتب‌سازی بر اساس"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "زمان شروع"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "وضعیت"
@@ -1250,9 +1346,15 @@ msgstr "فضای Swap استفاده شده توسط سیستم"
msgid "Swap Usage"
msgstr "میزان استفاده از Swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1264,7 +1366,7 @@ msgstr "میانگین بار سیستم در طول زمان"
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
msgstr ""
msgstr "خدمات Systemd"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1431,6 +1533,8 @@ msgstr "هنگامی که وضعیت بین بالا و پایین تغییر م
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "هنگامی که استفاده از هر دیسکی از یک آستانه فراتر رود، فعال می‌شود"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "نوع"
@@ -1469,7 +1573,12 @@ msgstr "فعال"
msgid "Up ({upSystemsLength})"
msgstr "فعال ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "به‌روزرسانی"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "به‌روزرسانی شد"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-11-11 19:25\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -34,7 +34,7 @@ msgstr "{count, plural, one {{countString} heure} other {{countString} heures}}"
#: src/lib/utils.ts
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
msgstr "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
msgstr "{count, plural, one {{countString} minute} other {{countString} minutes}}"
#: src/lib/utils.ts
msgid "1 hour"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Actions"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Active"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "État actif"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Ajouter <0>un Système</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Ajouter {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Ajouter un nouveau système"
msgid "Add <0>System</0>"
msgstr "Ajouter <0>un Système</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Ajouter lURL"
msgid "Adjust display options for charts."
msgstr "Ajuster les options d'affichage pour les graphiques."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Ajuster la largeur de la mise en page principale"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Tous les conteneurs"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Peut arrêter"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Annuler"
@@ -316,6 +327,12 @@ msgstr "Vérifiez les journaux pour plus de détails."
msgid "Check your notification service"
msgstr "Vérifiez votre service de notification"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Effacer"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Cliquez sur un conteneur pour voir plus d'informations."
@@ -438,6 +455,10 @@ msgstr "Répartition du temps CPU"
msgid "CPU Usage"
msgstr "Utilisation du CPU"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Créer"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Créer un compte"
@@ -469,15 +490,18 @@ msgstr "État actuel"
msgid "Cycles"
msgstr "Cycles"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Tableau de bord"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Quotidien"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Période par défaut"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Supprimer"
@@ -488,7 +512,7 @@ msgstr "Supprimer l'empreinte"
#: src/components/systemd-table/systemd-table.tsx
msgid "Description"
msgstr "Description"
msgstr ""
#: src/components/containers-table/containers-table.tsx
msgid "Detail"
@@ -562,11 +586,16 @@ msgstr "Télécharger"
msgid "Duration"
msgstr "Durée"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Éditer"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Modifier {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Notifications par email"
msgid "Empty"
msgstr "Vide"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Heure de fin"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Entrez l'adresse email pour réinitialiser le mot de passe"
@@ -598,6 +632,8 @@ msgstr "Entrez votre mot de passe à usage unique."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Échoué"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Attributs défaillants :"
@@ -648,6 +688,7 @@ msgstr "Échec de l'authentification"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Échec de l'enregistrement des paramètres"
@@ -706,6 +747,10 @@ msgstr "Pleine"
msgid "General"
msgstr "Général"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Global"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Moteurs GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Image"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inactif"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Adresse email invalide."
@@ -767,6 +816,10 @@ msgstr "Langue"
msgid "Layout"
msgstr "Disposition"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Largeur de la mise en page"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Cycle de vie"
@@ -947,12 +1000,19 @@ msgstr "Support OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Unique"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Mot de passe à usage unique"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Ouvrir le menu"
@@ -969,6 +1029,7 @@ msgstr "Autre"
msgid "Overwrite existing alerts"
msgstr "Écraser les alertes existantes"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Le mot de passe doit être inférieur à 72 Octets."
msgid "Password reset request received"
msgstr "Demande de réinitialisation du mot de passe reçue"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Passé"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pause"
@@ -1082,6 +1147,10 @@ msgstr "Processus démarré"
msgid "Public Key"
msgstr "Clé publique"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Heures calmes"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Reçu"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Actualiser"
@@ -1173,6 +1243,18 @@ msgstr "Enregistrer les paramètres"
msgid "Save system"
msgstr "Sauvegarder le système"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Programmer"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Programmez des heures calmes où les notifications ne seront pas envoyées, par exemple pendant les périodes de maintenance."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Programmez des heures calmes où les notifications ne seront pas envoyées."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Recherche"
@@ -1185,6 +1267,10 @@ msgstr "Rechercher des systèmes ou des paramètres..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Voir les <0>paramètres de notification</0> pour configurer comment vous recevez les alertes."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Sélectionner {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Envoyé"
@@ -1197,6 +1283,10 @@ msgstr "Numéro de série"
msgid "Service Details"
msgstr "Détails du service"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Services"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Définir des seuils de pourcentage pour les couleurs des compteurs."
@@ -1224,8 +1314,14 @@ msgstr "Paramètres SMTP"
msgid "Sort By"
msgstr "Trier par"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Heure de début"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "État"
@@ -1250,9 +1346,15 @@ msgstr "Espace Swap utilisé par le système"
msgid "Swap Usage"
msgstr "Utilisation du swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Se déclenche lorsque le statut passe de \"Joignable\" à \"Injoignable\
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Type"
@@ -1469,7 +1573,12 @@ msgstr "Joignable"
msgid "Up ({upSystemsLength})"
msgstr "Joignable ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Mettre à jour"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Mis à jour"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-30 21:53\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 דק'"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "פעולות"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "פעיל"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "מצב פעיל"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "הוסף <0>מערכת</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "הוסף {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "הוסף מערכת חדשה"
msgid "Add <0>System</0>"
msgstr "הוסף <0>מערכת</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "הוסף URL"
msgid "Adjust display options for charts."
msgstr "התאם אפשרויות תצוגה עבור גרפים."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "התאם את רוחב הפריסה הראשית"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "כל הקונטיינרים"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "יכול לעצור"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "ביטול"
@@ -316,6 +327,12 @@ msgstr "בדוק לוגים לפרטים נוספים"
msgid "Check your notification service"
msgstr "בדוק את שירות ההתראות שלך"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "נקה"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "לחץ על קונטיינר כדי לצפות במידע נוסף."
@@ -438,6 +455,10 @@ msgstr "פירוט זמן CPU"
msgid "CPU Usage"
msgstr "שימוש CPU"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "צור"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "צור חשבון"
@@ -469,15 +490,18 @@ msgstr "מצב נוכחי"
msgid "Cycles"
msgstr "מחזורים"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "לוח בקרה"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "יומי"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "תקופת זמן ברירת מחדל"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "מחק"
@@ -562,11 +586,16 @@ msgstr "הורדה"
msgid "Duration"
msgstr "משך זמן"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "ערוך"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "ערוך {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "התראות אימייל"
msgid "Empty"
msgstr "ריק"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "זמן סיום"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "הכנס כתובת אימייל לאיפוס סיסמה"
@@ -598,6 +632,8 @@ msgstr "הכנס את הסיסמה החד-פעמית שלך."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "ייצא את תצורת המערכות הנוכחית שלך."
msgid "Fahrenheit (°F)"
msgstr "פרנהייט (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "נכשל"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "מאפיינים שנכשלו:"
@@ -648,6 +688,7 @@ msgstr "אימות נכשל"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "שמירת הגדרות נכשלה"
@@ -706,6 +747,10 @@ msgstr "מלא"
msgid "General"
msgstr "כללי"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "גלובלי"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "מנועי GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "תמונה"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "לא פעיל"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "כתובת אימייל לא תקינה."
@@ -767,6 +816,10 @@ msgstr "שפה"
msgid "Layout"
msgstr "פריסה"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "רוחב פריסה"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "מחזור חיים"
@@ -947,12 +1000,19 @@ msgstr "תמיכה ב-OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "בכל הפעלה מחדש, המערכות במסד הנתונים יעודכנו כדי להתאים למערכות המוגדרות בקובץ."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "חד-פעמי"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "סיסמה חד-פעמית"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "פתח תפריט"
@@ -969,6 +1029,7 @@ msgstr "אחר"
msgid "Overwrite existing alerts"
msgstr "דרוס התראות קיימות"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "הסיסמה חייבת להיות פחות מ-72 בתים."
msgid "Password reset request received"
msgstr "בקשת איפוס סיסמה התקבלה"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "עבר"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "השהה"
@@ -1082,6 +1147,10 @@ msgstr "תהליך התחיל"
msgid "Public Key"
msgstr "מפתח ציבורי"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "שעות שקט"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "התקבל"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "רענן"
@@ -1173,6 +1243,18 @@ msgstr "שמור הגדרות"
msgid "Save system"
msgstr "שמור מערכת"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "לוח זמנים"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "קבע שעות שקט שבהן לא יישלחו התראות, כמו במהלך תקופות תחזוקה."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "קבע שעות שקט שבהן לא יישלחו התראות."
#: src/components/navbar.tsx
msgid "Search"
msgstr "חיפוש"
@@ -1185,6 +1267,10 @@ msgstr "חפש מערכות או הגדרות..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "ראה <0>הגדרות התראות</0> כדי להגדיר כיצד אתה מקבל התראות."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "בחר {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "נשלח"
@@ -1197,6 +1283,10 @@ msgstr "מספר סידורי"
msgid "Service Details"
msgstr "פרטי שירות"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "שירותים"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "הגדר סף אחוזים עבור צבעי מד."
@@ -1224,8 +1314,14 @@ msgstr "הגדרות SMTP"
msgid "Sort By"
msgstr "מיין לפי"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "זמן התחלה"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "מצב"
@@ -1250,9 +1346,15 @@ msgstr "שטח swap בשימוש על ידי המערכת"
msgid "Swap Usage"
msgstr "שימוש ב-Swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "מופעל כאשר הסטטוס מתחלף בין למעלה ולמטה
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "מופעל כאשר שימוש בכל דיסק עולה על סף"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "סוג"
@@ -1469,7 +1573,12 @@ msgstr "למעלה"
msgid "Up ({upSystemsLength})"
msgstr "למעלה ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "עדכן"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "עודכן"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hr\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \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"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 minuta"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Akcije"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktivan"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Aktivno stanje"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Dodaj <0>Sistem</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr ""
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Dodaj Novi Sistem"
msgid "Add <0>System</0>"
msgstr "Dodaj <0>Sistem</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Dodaj URL"
msgid "Adjust display options for charts."
msgstr "Podesite opcije prikaza za grafikone."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Prilagodite širinu glavnog rasporeda"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Svi spremnici"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Može se zaustaviti"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Otkaži"
@@ -316,6 +327,12 @@ msgstr "Provjerite logove za više detalja."
msgid "Check your notification service"
msgstr "Provjerite Vaš servis notifikacija"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Očisti"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Kliknite na spremnik za prikaz više informacija."
@@ -438,6 +455,10 @@ msgstr "Raspodjela CPU vremena"
msgid "CPU Usage"
msgstr "Iskorištenost procesora"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Stvori"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Napravite račun"
@@ -469,15 +490,18 @@ msgstr "Trenutno stanje"
msgid "Cycles"
msgstr "Ciklusi"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Nadzorna ploča"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Dnevno"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Zadano vremensko razdoblje"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Izbriši"
@@ -562,11 +586,16 @@ msgstr "Preuzmi"
msgid "Duration"
msgstr "Trajanje"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Uredi"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Email notifikacije"
msgid "Empty"
msgstr "Prazna"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Vrijeme završetka"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Unesite email adresu za resetiranje lozinke"
@@ -598,6 +632,8 @@ msgstr "Unesite Vašu jednokratnu lozinku."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Izvoz trenutne sistemske konfiguracije."
msgid "Fahrenheit (°F)"
msgstr "Farenhajt (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Neuspješno"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Neuspjeli atributi:"
@@ -648,6 +688,7 @@ msgstr "Provjera autentičnosti nije uspjela"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Neuspješno snimanje postavki"
@@ -706,6 +747,10 @@ msgstr "Puna"
msgid "General"
msgstr "Općenito"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Globalno"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU motori"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Slika"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Neaktivno"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Nevažeća adresa e-pošte."
@@ -767,6 +816,10 @@ msgstr "Jezik"
msgid "Layout"
msgstr "Izgled"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Širina rasporeda"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Životni ciklus"
@@ -947,12 +1000,19 @@ msgstr "Podrška za OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka biti će ažurirani kako bi odgovarali sustavima definiranim u datoteci."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Jednokratno"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Jednokratna lozinka"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Otvori menu"
@@ -969,6 +1029,7 @@ msgstr "Ostalo"
msgid "Overwrite existing alerts"
msgstr "Prebrišite postojeća upozorenja"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "Lozinka mora biti kraća od 72 bajta."
msgid "Password reset request received"
msgstr "Zahtjev za ponovno postavljanje lozinke primljen"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Prošlost"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pauza"
@@ -1082,6 +1147,10 @@ msgstr "Proces pokrenut"
msgid "Public Key"
msgstr "Javni Ključ"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Tihi sati"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Primljeno"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Osvježi"
@@ -1173,6 +1243,18 @@ msgstr "Spremi Postavke"
msgid "Save system"
msgstr "Spremi sustav"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Raspored"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Rasporedi tihe sate kada se obavijesti neće slati, na primjer tijekom razdoblja održavanja."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Rasporedi tihe sate kada se obavijesti neće slati."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Pretraži"
@@ -1185,6 +1267,10 @@ msgstr "Pretraži za sisteme ili postavke..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način primanja upozorenja."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr ""
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Poslano"
@@ -1197,6 +1283,10 @@ msgstr "Serijski broj"
msgid "Service Details"
msgstr "Detalji usluge"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Usluge"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Postavite pragove postotka za boje mjerača."
@@ -1224,8 +1314,14 @@ msgstr "SMTP postavke"
msgid "Sort By"
msgstr "Sortiraj po"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Vrijeme početka"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Stanje"
@@ -1250,9 +1346,15 @@ msgstr "Swap prostor uzet od strane sistema"
msgid "Swap Usage"
msgstr "Swap Iskorištenost"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Pokreće se kada se status sistema promijeni"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Vrsta"
@@ -1469,7 +1573,12 @@ msgstr "Sustav je podignut"
msgid "Up ({upSystemsLength})"
msgstr "Sustav je podignut ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Ažuriraj"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Ažurirano"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 22:59\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 perc"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Műveletek"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Aktív"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Aktív állapot"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Hozzáadás <0>System</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr ""
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Új rendszer hozzáadása"
msgid "Add <0>System</0>"
msgstr "Hozzáadás <0>System</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "URL hozzáadása"
msgid "Adjust display options for charts."
msgstr "Állítsa be a diagram megjelenítését."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "A fő elrendezés szélességének beállítása"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Összes konténer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Leállítható"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Mégsem"
@@ -316,6 +327,12 @@ msgstr "Ellenőrizd a naplót a további részletekért."
msgid "Check your notification service"
msgstr "Ellenőrizd az értesítési szolgáltatásodat"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Törlés"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Kattintson egy konténerre a további információk megtekintéséhez."
@@ -438,6 +455,10 @@ msgstr "CPU idő felbontása"
msgid "CPU Usage"
msgstr "CPU használat"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Létrehozás"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Fiók létrehozása"
@@ -469,15 +490,18 @@ msgstr "Jelenlegi állapot"
msgid "Cycles"
msgstr "Ciklusok"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Áttekintés"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Napi"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Alapértelmezett időszak"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Törlés"
@@ -562,11 +586,16 @@ msgstr "Letöltés"
msgid "Duration"
msgstr "Időtartam"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Szerkesztés"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "E-mail értesítések"
msgid "Empty"
msgstr "Üres"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Befejezés ideje"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "E-mail cím megadása a jelszó visszaállításához"
@@ -598,6 +632,8 @@ msgstr "Adja meg az egyszeri jelszavát."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Sikertelen"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Sikertelen attribútumok:"
@@ -648,6 +688,7 @@ msgstr "Hitelesítés sikertelen"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Nem sikerült menteni a beállításokat"
@@ -706,6 +747,10 @@ msgstr "Tele"
msgid "General"
msgstr "Általános"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Globális"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU-k"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Kép"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inaktív"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Érvénytelen e-mail cím."
@@ -767,6 +816,10 @@ msgstr "Nyelv"
msgid "Layout"
msgstr "Elrendezés"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Elrendezés szélessége"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Életciklus"
@@ -947,12 +1000,19 @@ msgstr "OAuth 2 / OIDC támogatás"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Minden újraindításkor az adatbázisban lévő rendszerek frissítésre kerülnek, hogy megfeleljenek a fájlban meghatározott rendszereknek."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Egyszeri"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Egyszeri jelszó"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Menü megnyitása"
@@ -969,6 +1029,7 @@ msgstr "Egyéb"
msgid "Overwrite existing alerts"
msgstr "Felülírja a meglévő riasztásokat"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "A jelszó legfeljebb 72 byte lehet."
msgid "Password reset request received"
msgstr "Jelszó-visszaállítási kérelmet kaptunk"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Múlt"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Szüneteltetés"
@@ -1082,6 +1147,10 @@ msgstr "Folyamat elindítva"
msgid "Public Key"
msgstr "Nyilvános kulcs"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Csendes órák"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Fogadott"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Frissítés"
@@ -1173,6 +1243,18 @@ msgstr "Beállítások mentése"
msgid "Save system"
msgstr "Rendszer mentése"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Ütemezés"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Ütemezze a csendes órákat, amikor az értesítések nem kerülnek elküldésre, például karbantartási időszakokban."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Ütemezze a csendes órákat, amikor az értesítések nem kerülnek elküldésre."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Keresés"
@@ -1185,6 +1267,10 @@ msgstr "Keresés rendszerek vagy beállítások után..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogyan kap értesítéseket."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr ""
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Elküldve"
@@ -1197,6 +1283,10 @@ msgstr "Sorozatszám"
msgid "Service Details"
msgstr "Szolgáltatás részletei"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Szolgáltatások"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Százalékos küszöbértékek beállítása a mérőszínekhez."
@@ -1224,8 +1314,14 @@ msgstr "SMTP beállítások"
msgid "Sort By"
msgstr "Rendezés"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Kezdési idő"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Állapot"
@@ -1250,9 +1346,15 @@ msgstr "Rendszer által használt swap terület"
msgid "Swap Usage"
msgstr "Swap használat"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Bekapcsol, amikor az állapot fel és le között változik"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Bekapcsol, ha a lemez érzékelő túllép egy küszöbértéket"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Típus"
@@ -1469,7 +1573,12 @@ msgstr "Online"
msgid "Up ({upSystemsLength})"
msgstr "Online ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Frissítés"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Frissítve"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-30 21:53\n"
"PO-Revision-Date: 2025-11-20 16:58\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5 min"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "Azioni"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "Attivo"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "Stato attivo"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "Aggiungi <0>Sistema</0>"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "Aggiungi {foo}"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "Aggiungi Nuovo Sistema"
msgid "Add <0>System</0>"
msgstr "Aggiungi <0>Sistema</0>"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "Aggiungi URL"
msgid "Adjust display options for charts."
msgstr "Regola le opzioni di visualizzazione per i grafici."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "Regola la larghezza del layout principale"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "Tutti i contenitori"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "Può fermare"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "Annulla"
@@ -316,6 +327,12 @@ msgstr "Controlla i log per maggiori dettagli."
msgid "Check your notification service"
msgstr "Controlla il tuo servizio di notifica"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "Cancella"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "Fare clic su un contenitore per visualizzare ulteriori informazioni."
@@ -438,6 +455,10 @@ msgstr "Suddivisione tempo CPU"
msgid "CPU Usage"
msgstr "Utilizzo CPU"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "Crea"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "Crea account"
@@ -469,15 +490,18 @@ msgstr "Stato attuale"
msgid "Cycles"
msgstr "Cicli"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "Cruscotto"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "Giornaliero"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "Periodo di tempo predefinito"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "Elimina"
@@ -562,11 +586,16 @@ msgstr "Scarica"
msgid "Duration"
msgstr "Durata"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "Modifica"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "Modifica {foo}"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "Notifiche email"
msgid "Empty"
msgstr "Vuota"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "Ora di fine"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "Inserisci l'indirizzo email per reimpostare la password"
@@ -598,6 +632,8 @@ msgstr "Inserisci la tua password monouso."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "Fallito"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "Attributi falliti:"
@@ -648,6 +688,7 @@ msgstr "Autenticazione fallita"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "Salvataggio delle impostazioni fallito"
@@ -706,6 +747,10 @@ msgstr "Piena"
msgid "General"
msgstr "Generale"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "Globale"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "Motori GPU"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "Immagine"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "Inattivo"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "Indirizzo email non valido."
@@ -767,6 +816,10 @@ msgstr "Lingua"
msgid "Layout"
msgstr "Aspetto"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "Larghezza del layout"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "Ciclo di vita"
@@ -908,7 +961,7 @@ msgstr "Unità rete"
#: src/components/systemd-table/systemd-table.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "No"
msgstr "No"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/systemd-table/systemd-table.tsx
@@ -947,12 +1000,19 @@ msgstr "Supporto OAuth 2 / OIDC"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "Ad ogni riavvio, i sistemi nel database verranno aggiornati per corrispondere ai sistemi definiti nel file."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "Una volta"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "Password monouso"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "Apri menu"
@@ -969,6 +1029,7 @@ msgstr "Altro"
msgid "Overwrite existing alerts"
msgstr "Sovrascrivi avvisi esistenti"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "La password deve essere inferiore a 72 byte."
msgid "Password reset request received"
msgstr "Richiesta di reimpostazione password ricevuta"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "Passato"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "Pausa"
@@ -1082,6 +1147,10 @@ msgstr "Processo avviato"
msgid "Public Key"
msgstr "Chiave Pub"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "Ore silenziose"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "Ricevuto"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "Aggiorna"
@@ -1173,6 +1243,18 @@ msgstr "Salva Impostazioni"
msgid "Save system"
msgstr "Salva sistema"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "Pianifica"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "Pianifica le ore silenziose in cui le notifiche non verranno inviate, ad esempio durante i periodi di manutenzione."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "Pianifica le ore silenziose in cui le notifiche non verranno inviate."
#: src/components/navbar.tsx
msgid "Search"
msgstr "Cerca"
@@ -1185,6 +1267,10 @@ msgstr "Cerca sistemi o impostazioni..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Vedi <0>impostazioni di notifica</0> per configurare come ricevere gli avvisi."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "Seleziona {foo}"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "Inviato"
@@ -1197,6 +1283,10 @@ msgstr "Numero di serie"
msgid "Service Details"
msgstr "Dettagli servizio"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "Servizi"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Imposta le soglie percentuali per i colori dei contatori."
@@ -1224,8 +1314,14 @@ msgstr "Impostazioni SMTP"
msgid "Sort By"
msgstr "Ordina per"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "Ora di inizio"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "Stato"
@@ -1250,9 +1346,15 @@ msgstr "Spazio di swap utilizzato dal sistema"
msgid "Swap Usage"
msgstr "Utilizzo Swap"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "Attiva quando lo stato passa tra up e down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "Tipo"
@@ -1469,7 +1573,12 @@ msgstr "Attivo"
msgid "Up ({upSystemsLength})"
msgstr "Attivo ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "Aggiorna"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "Aggiornato"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5分"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "アクション"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "アクティブ"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "アクティブ状態"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>システム</0>を追加"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "{foo}を追加"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "新しいシステムを追加"
msgid "Add <0>System</0>"
msgstr "<0>システム</0>を追加"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "URLを追加"
msgid "Adjust display options for charts."
msgstr "チャートの表示オプションを調整します。"
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "メインレイアウトの幅を調整"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "すべてのコンテナ"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "停止可能"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "キャンセル"
@@ -316,6 +327,12 @@ msgstr "詳細についてはログを確認してください。"
msgid "Check your notification service"
msgstr "通知サービスを確認してください"
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "クリア"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "詳細情報を表示するにはコンテナをクリックしてください。"
@@ -438,6 +455,10 @@ msgstr "CPU 時間の内訳"
msgid "CPU Usage"
msgstr "CPU使用率"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "作成"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "アカウントを作成"
@@ -469,15 +490,18 @@ msgstr "現在の状態"
msgid "Cycles"
msgstr "サイクル"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "ダッシュボード"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "毎日"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "デフォルトの期間"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "削除"
@@ -562,11 +586,16 @@ msgstr "ダウンロード"
msgid "Duration"
msgstr "期間"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "編集"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "{foo}を編集"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "メール通知"
msgid "Empty"
msgstr "空"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "終了時間"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "パスワードをリセットするためにメールアドレスを入力してください"
@@ -598,6 +632,8 @@ msgstr "ワンタイムパスワードを入力してください。"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "現在のシステム設定をエクスポートします。"
msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "失敗"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "失敗した属性:"
@@ -648,6 +688,7 @@ msgstr "認証に失敗しました"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "設定の保存に失敗しました"
@@ -706,6 +747,10 @@ msgstr "満充電"
msgid "General"
msgstr "一般"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "グローバル"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPUエンジン"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "イメージ"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "非アクティブ"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "無効なメールアドレスです。"
@@ -767,6 +816,10 @@ msgstr "言語"
msgid "Layout"
msgstr "レイアウト"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "レイアウト幅"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "ライフサイクル"
@@ -947,12 +1000,19 @@ msgstr "OAuth 2 / OIDCサポート"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "再起動のたびに、データベース内のシステムはファイルに定義されたシステムに一致するように更新されます。"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "1回限り"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "ワンタイムパスワード"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "メニューを開く"
@@ -969,6 +1029,7 @@ msgstr "その他"
msgid "Overwrite existing alerts"
msgstr "既存のアラートを上書き"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "パスワードは72バイト未満でなければなりません。"
msgid "Password reset request received"
msgstr "パスワードリセットのリクエストを受け取りました"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "過去"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "一時停止"
@@ -1082,6 +1147,10 @@ msgstr "プロセス開始"
msgid "Public Key"
msgstr "公開鍵"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "サイレント時間"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "受信"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "更新"
@@ -1138,7 +1208,7 @@ msgstr "再開"
#: src/components/systems-table/systems-table-columns.tsx
msgctxt "Root disk label"
msgid "Root"
msgstr ""
msgstr "ルート"
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Rotate token"
@@ -1173,6 +1243,18 @@ msgstr "設定を保存"
msgid "Save system"
msgstr "システムを保存"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "スケジュール"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "メンテナンス期間中などの通知が送信されないサイレント時間をスケジュールします。"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "通知が送信されないサイレント時間をスケジュールします。"
#: src/components/navbar.tsx
msgid "Search"
msgstr "検索"
@@ -1185,6 +1267,10 @@ msgstr "システムまたは設定を検索..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "アラートの受信方法を設定するには<0>通知設定</0>を参照してください。"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "{foo}を選択"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "送信"
@@ -1197,6 +1283,10 @@ msgstr "シリアル番号"
msgid "Service Details"
msgstr "サービス詳細"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "サービス"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "メーターの色を変更するしきい値(パーセンテージ)を設定します。"
@@ -1224,8 +1314,14 @@ msgstr "SMTP設定"
msgid "Sort By"
msgstr "並び替え基準"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "開始時間"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "状態"
@@ -1250,9 +1346,15 @@ msgstr "システムが使用するスワップ領域"
msgid "Swap Usage"
msgstr "スワップ使用量"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1431,6 +1533,8 @@ msgstr "ステータスが上から下に切り替わるときにトリガーさ
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "ディスクの使用量がしきい値を超えたときにトリガーされます"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "タイプ"
@@ -1469,7 +1573,12 @@ msgstr "正常"
msgid "Up ({upSystemsLength})"
msgstr "正常 ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "更新"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "更新済み"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-28 23:00\n"
"PO-Revision-Date: 2025-11-14 22:51\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -76,13 +76,16 @@ msgid "5 min"
msgstr "5분"
#. Table column
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Actions"
msgstr "작업"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Active"
msgstr "활성"
@@ -95,12 +98,14 @@ msgid "Active state"
msgstr "활성 상태"
#: src/components/add-system.tsx
msgid "Add <0>System</0>"
msgstr "<0>시스템</0> 추가"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Add {foo}"
msgstr "{foo} 추가"
#: src/components/add-system.tsx
msgid "Add New System"
msgstr "새 시스템 추가"
msgid "Add <0>System</0>"
msgstr "<0>시스템</0> 추가"
#: src/components/add-system.tsx
msgid "Add system"
@@ -114,6 +119,10 @@ msgstr "URL 추가"
msgid "Adjust display options for charts."
msgstr "차트 표시 옵션 변경."
#: src/components/routes/settings/general.tsx
msgid "Adjust the width of the main layout"
msgstr "메인 레이아웃 너비 조정"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
@@ -147,6 +156,7 @@ msgstr "모든 컨테이너"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
@@ -263,6 +273,7 @@ msgid "Can stop"
msgstr "중지 가능"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Cancel"
msgstr "취소"
@@ -316,6 +327,12 @@ msgstr "자세한 내용은 로그를 확인하세요."
msgid "Check your notification service"
msgstr "알림 서비스를 확인하세요."
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Clear"
msgstr "지우기"
#: src/components/containers-table/containers-table.tsx
msgid "Click on a container to view more information."
msgstr "더 많은 정보를 보려면 컨테이너를 클릭하세요."
@@ -438,6 +455,10 @@ msgstr "CPU 시간 분배"
msgid "CPU Usage"
msgstr "CPU 사용량"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Create"
msgstr "생성"
#: src/components/login/auth-form.tsx
msgid "Create account"
msgstr "계정 생성"
@@ -469,15 +490,18 @@ msgstr "현재 상태"
msgid "Cycles"
msgstr "사이클"
#: src/components/command-palette.tsx
msgid "Dashboard"
msgstr "대시보드"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Daily"
msgstr "매일"
#: src/components/routes/settings/general.tsx
msgid "Default time period"
msgstr "기본 기간"
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Delete"
msgstr "삭제"
@@ -562,11 +586,16 @@ msgstr "다운로드"
msgid "Duration"
msgstr "기간"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Edit"
msgstr "수정"
#: src/components/add-system.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Edit {foo}"
msgstr "{foo} 수정"
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
@@ -582,6 +611,11 @@ msgstr "이메일 알림"
msgid "Empty"
msgstr "빔"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "End Time"
msgstr "종료 시간"
#: src/components/login/login.tsx
msgid "Enter email address to reset password"
msgstr "비밀번호를 재설정하려면 이메일 주소를 입력하세요"
@@ -598,6 +632,8 @@ msgstr "OTP를 입력하세요."
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systemd-table/systemd-table.tsx
msgid "Error"
@@ -638,6 +674,10 @@ msgstr "현재 시스템 구성 내보내기"
msgid "Fahrenheit (°F)"
msgstr "화씨 (°F)"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Failed"
msgstr "실패"
#: src/components/routes/system/smart-table.tsx
msgid "Failed Attributes:"
msgstr "실패한 속성:"
@@ -648,6 +688,7 @@ msgstr "인증 실패"
#: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/notifications.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Failed to save settings"
msgstr "설정 저장 실패"
@@ -706,6 +747,10 @@ msgstr "가득"
msgid "General"
msgstr "일반"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Global"
msgstr "전역"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr "GPU 엔진들"
@@ -750,6 +795,10 @@ msgctxt "Docker image"
msgid "Image"
msgstr "이미지"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Inactive"
msgstr "비활성"
#: src/components/login/auth-form.tsx
msgid "Invalid email address."
msgstr "잘못된 이메일 주소입니다."
@@ -767,6 +816,10 @@ msgstr "언어"
msgid "Layout"
msgstr "레이아웃"
#: src/components/routes/settings/general.tsx
msgid "Layout width"
msgstr "레이아웃 너비"
#: src/components/systemd-table/systemd-table.tsx
msgid "Lifecycle"
msgstr "생명주기"
@@ -947,12 +1000,19 @@ msgstr "OAuth 2 / OIDC 지원"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr "매 시작 시, 데이터베이스가 파일에 정의된 시스템과 일치하도록 업데이트됩니다."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "One-time"
msgstr "일회성"
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr "OTP"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
msgid "Open menu"
msgstr "메뉴 열기"
@@ -969,6 +1029,7 @@ msgstr "기타"
msgid "Overwrite existing alerts"
msgstr "기존 알림 덮어쓰기"
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Page"
@@ -1001,6 +1062,10 @@ msgstr "비밀번호는 72 바이트 이하여야 합니다."
msgid "Password reset request received"
msgstr "비밀번호 재설정 요청이 접수되었습니다"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Past"
msgstr "과거"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Pause"
msgstr "일시 중지"
@@ -1082,6 +1147,10 @@ msgstr "프로세스 시작됨"
msgid "Public Key"
msgstr "공개 키"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Quiet Hours"
msgstr "조용한 시간"
#. Disk read
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -1094,6 +1163,7 @@ msgstr "수신됨"
#: src/components/containers-table/containers-table.tsx
#: src/components/containers-table/containers-table.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Refresh"
msgstr "새로고침"
@@ -1173,6 +1243,18 @@ msgstr "설정 저장"
msgid "Save system"
msgstr "시스템 저장"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule"
msgstr "일정"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent, such as during maintenance periods."
msgstr "유지보수 기간 등 알림이 전송되지 않을 조용한 시간을 예약하세요."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Schedule quiet hours where notifications will not be sent."
msgstr "알림이 전송되지 않을 조용한 시간을 예약하세요."
#: src/components/navbar.tsx
msgid "Search"
msgstr "검색"
@@ -1185,6 +1267,10 @@ msgstr "시스템 또는 설정 검색..."
msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "알림을 받는 방법을 구성하려면 <0>알림 설정</0>을 참조하세요."
#: src/components/routes/settings/quiet-hours.tsx
msgid "Select {foo}"
msgstr "{foo} 선택"
#: src/components/routes/system.tsx
msgid "Sent"
msgstr "보냄"
@@ -1197,6 +1283,10 @@ msgstr "시리얼 번호"
msgid "Service Details"
msgstr "서비스 세부 정보"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Services"
msgstr "서비스"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "그래프 미터 색상의 백분율 임계값을 설정합니다."
@@ -1224,8 +1314,14 @@ msgstr "SMTP 설정"
msgid "Sort By"
msgstr "정렬 기준"
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
msgid "Start Time"
msgstr "시작 시간"
#. Context: alert state (active or resolved)
#: src/components/alerts-history-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "State"
msgstr "상태"
@@ -1250,9 +1346,15 @@ msgstr "시스템에서 사용된 스왑 공간"
msgid "Swap Usage"
msgstr "스왑 사용량"
#: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/lib/alerts.ts
msgid "System"
@@ -1264,7 +1366,7 @@ msgstr "시간에 따른 시스템 부하 평균"
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
msgstr ""
msgstr "Systemd 서비스"
#: src/components/navbar.tsx
msgid "Systems"
@@ -1431,6 +1533,8 @@ msgstr "시스템의 전원이 켜지거나 꺼질때 트리거됩니다."
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/system/smart-table.tsx
msgid "Type"
msgstr "유형"
@@ -1469,7 +1573,12 @@ msgstr "온라인"
msgid "Up ({upSystemsLength})"
msgstr "온라인 ({upSystemsLength})"
#: src/components/routes/settings/quiet-hours.tsx
msgid "Update"
msgstr "업데이트"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/routes/system/smart-table.tsx
#: src/components/systemd-table/systemd-table-columns.tsx
msgid "Updated"
msgstr "업데이트됨"

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