mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-01 12:16:00 +00:00
Compare commits
12
Commits
2341-lxc-cpu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0952ffcf | ||
|
|
2502dd9626 | ||
|
|
8a91a0b5ab | ||
|
|
6ff9b4cfea | ||
|
|
65e3e2e768 | ||
|
|
87f1efdfd9 | ||
|
|
dd3a769d18 | ||
|
|
266f45f104 | ||
|
|
e925fafd7b | ||
|
|
15c29fee92 | ||
|
|
2ca5e727f9 | ||
|
|
e01e2a20e9 |
+10
-4
@@ -679,11 +679,11 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Previous snapshot for this interval and device
|
// Previous snapshot for this interval and device
|
||||||
prev, hasPrev := a.diskPrev[cacheTimeMs][name]
|
prev, ok := a.diskPrev[cacheTimeMs][name]
|
||||||
if !hasPrev {
|
firstSample := !ok
|
||||||
|
if firstSample {
|
||||||
// Seed from the latest counters of any interval, else seed from current
|
// Seed from the latest counters of any interval, else seed from current
|
||||||
prev, hasPrev = a.diskBaseline[name]
|
if prev, ok = a.diskBaseline[name]; !ok {
|
||||||
if !hasPrev {
|
|
||||||
prev = prevDiskFromCounter(d, now)
|
prev = prevDiskFromCounter(d, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -697,6 +697,12 @@ func (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {
|
|||||||
if msElapsed < 100 {
|
if msElapsed < 100 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// The first sample of an interval must span at least half the interval.
|
||||||
|
// Right after agent start the baseline is only a second or so old, and a
|
||||||
|
// burst of startup I/O would be recorded as the rate for the whole interval.
|
||||||
|
if firstSample && msElapsed < uint64(cacheTimeMs)/2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
diskIORead := (d.ReadBytes - prev.readBytes) * 1000 / msElapsed
|
diskIORead := (d.ReadBytes - prev.readBytes) * 1000 / msElapsed
|
||||||
diskIOWrite := (d.WriteBytes - prev.writeBytes) * 1000 / msElapsed
|
diskIOWrite := (d.WriteBytes - prev.writeBytes) * 1000 / msElapsed
|
||||||
|
|||||||
@@ -75,17 +75,30 @@ func TestUpdateDiskIoTimeCounterWrap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first sample of a cache interval has no snapshot of its own. It must
|
// backdateDiskBaseline moves the baseline of a device into the past so the
|
||||||
// measure the time counters from the same baseline as the byte counters.
|
// next seeded sample of an interval spans d.
|
||||||
func TestUpdateDiskIoFirstSampleOfInterval(t *testing.T) {
|
func backdateDiskBaseline(a *Agent, name string, d time.Duration) {
|
||||||
|
b := a.diskBaseline[name]
|
||||||
|
b.at = time.Now().Add(-d)
|
||||||
|
a.diskBaseline[name] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupDiskstats points gopsutil at a temp dir and returns a writer for its diskstats file.
|
||||||
|
func setupDiskstats(t *testing.T) func(line string) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
t.Setenv("HOST_PROC", dir)
|
t.Setenv("HOST_PROC", dir)
|
||||||
t.Setenv("HOST_SYS", dir)
|
t.Setenv("HOST_SYS", dir)
|
||||||
t.Setenv("HOST_DEV", dir)
|
t.Setenv("HOST_DEV", dir)
|
||||||
t.Setenv("HOST_RUN", dir)
|
t.Setenv("HOST_RUN", dir)
|
||||||
writeDiskstats := func(line string) {
|
return func(line string) {
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "diskstats"), []byte(line), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "diskstats"), []byte(line), 0o644))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first sample of a cache interval has no snapshot of its own. It must
|
||||||
|
// measure the time counters from the same baseline as the byte counters.
|
||||||
|
func TestUpdateDiskIoFirstSampleOfInterval(t *testing.T) {
|
||||||
|
writeDiskstats := setupDiskstats(t)
|
||||||
|
|
||||||
writeDiskstats(" 8 0 sda 1000 0 20000 900 500 0 10000 700 0 400 0\n")
|
writeDiskstats(" 8 0 sda 1000 0 20000 900 500 0 10000 700 0 400 0\n")
|
||||||
counters, err := disk.IOCounters("sda")
|
counters, err := disk.IOCounters("sda")
|
||||||
@@ -97,9 +110,7 @@ func TestUpdateDiskIoFirstSampleOfInterval(t *testing.T) {
|
|||||||
diskPrev: map[uint16]map[string]prevDisk{},
|
diskPrev: map[uint16]map[string]prevDisk{},
|
||||||
}
|
}
|
||||||
a.initializeDiskIoStats(counters)
|
a.initializeDiskIoStats(counters)
|
||||||
|
backdateDiskBaseline(a, "sda", 60*time.Second)
|
||||||
// updateDiskIo skips samples less than 100ms apart.
|
|
||||||
time.Sleep(150 * time.Millisecond)
|
|
||||||
|
|
||||||
// Deltas: read 300ms / 10 ops, write 400ms / 20 ops, io time 1200ms, weighted io 3000ms.
|
// Deltas: read 300ms / 10 ops, write 400ms / 20 ops, io time 1200ms, weighted io 3000ms.
|
||||||
writeDiskstats(" 8 0 sda 1010 0 21200 1200 520 0 10400 1100 0 1600 3000\n")
|
writeDiskstats(" 8 0 sda 1010 0 21200 1200 520 0 10400 1100 0 1600 3000\n")
|
||||||
@@ -115,7 +126,7 @@ func TestUpdateDiskIoFirstSampleOfInterval(t *testing.T) {
|
|||||||
assert.NotZero(t, fs.DiskIoStats[5], "weighted io")
|
assert.NotZero(t, fs.DiskIoStats[5], "weighted io")
|
||||||
|
|
||||||
// A second interval starts from the latest counters, not from the ones at start.
|
// A second interval starts from the latest counters, not from the ones at start.
|
||||||
time.Sleep(150 * time.Millisecond)
|
backdateDiskBaseline(a, "sda", time.Second)
|
||||||
// Deltas: read 100ms / 10 ops, write 100ms / 20 ops.
|
// Deltas: read 100ms / 10 ops, write 100ms / 20 ops.
|
||||||
writeDiskstats(" 8 0 sda 1020 0 22400 1300 540 0 10800 1200 0 1800 3500\n")
|
writeDiskstats(" 8 0 sda 1020 0 22400 1300 540 0 10800 1200 0 1800 3500\n")
|
||||||
a.updateDiskIo(1000, &stats)
|
a.updateDiskIo(1000, &stats)
|
||||||
@@ -123,3 +134,41 @@ func TestUpdateDiskIoFirstSampleOfInterval(t *testing.T) {
|
|||||||
assert.InDelta(t, 10, fs.DiskIoStats[3], 0.01, "r_await")
|
assert.InDelta(t, 10, fs.DiskIoStats[3], 0.01, "r_await")
|
||||||
assert.InDelta(t, 5, fs.DiskIoStats[4], 0.01, "w_await")
|
assert.InDelta(t, 5, fs.DiskIoStats[4], 0.01, "w_await")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right after agent start the baseline is too recent to stand for a whole
|
||||||
|
// interval. The first sample only stores a snapshot, and the next one is
|
||||||
|
// measured from it.
|
||||||
|
func TestUpdateDiskIoSkipsShortSeededSample(t *testing.T) {
|
||||||
|
writeDiskstats := setupDiskstats(t)
|
||||||
|
|
||||||
|
writeDiskstats(" 8 0 sda 1000 0 20000 900 500 0 10000 700 0 400 0\n")
|
||||||
|
counters, err := disk.IOCounters("sda")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fs := &system.FsStats{Root: true}
|
||||||
|
a := &Agent{
|
||||||
|
fsStats: map[string]*system.FsStats{"sda": fs},
|
||||||
|
diskPrev: map[uint16]map[string]prevDisk{},
|
||||||
|
}
|
||||||
|
a.initializeDiskIoStats(counters)
|
||||||
|
backdateDiskBaseline(a, "sda", 2*time.Second)
|
||||||
|
|
||||||
|
// 1000 MB read in the 2s after start.
|
||||||
|
writeDiskstats(" 8 0 sda 2000 0 2068000 900 500 0 10000 700 0 400 0\n")
|
||||||
|
var stats system.Stats
|
||||||
|
a.updateDiskIo(60000, &stats)
|
||||||
|
|
||||||
|
assert.Zero(t, fs.DiskReadBytes)
|
||||||
|
assert.Zero(t, stats.DiskIO[0])
|
||||||
|
require.Contains(t, a.diskPrev[60000], "sda", "snapshot is stored")
|
||||||
|
|
||||||
|
// Next sample is measured from the stored snapshot: 60 MB over 60s.
|
||||||
|
snap := a.diskPrev[60000]["sda"]
|
||||||
|
snap.at = time.Now().Add(-60 * time.Second)
|
||||||
|
a.diskPrev[60000]["sda"] = snap
|
||||||
|
writeDiskstats(" 8 0 sda 3000 0 2190880 900 500 0 10000 700 0 400 0\n")
|
||||||
|
stats = system.Stats{}
|
||||||
|
a.updateDiskIo(60000, &stats)
|
||||||
|
|
||||||
|
assert.InDelta(t, 1_048_576, float64(stats.DiskIO[0]), 20_000)
|
||||||
|
}
|
||||||
|
|||||||
@@ -866,6 +866,7 @@ func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID strin
|
|||||||
// getLogs fetches the logs for a container
|
// getLogs fetches the logs for a container
|
||||||
func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {
|
func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {
|
||||||
query := url.Values{
|
query := url.Values{
|
||||||
|
"timestamps": []string{"1"},
|
||||||
"stdout": []string{"1"},
|
"stdout": []string{"1"},
|
||||||
"stderr": []string{"1"},
|
"stderr": []string{"1"},
|
||||||
"tail": []string{fmt.Sprintf("%d", dockerLogsTail)},
|
"tail": []string{fmt.Sprintf("%d", dockerLogsTail)},
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ interval only; real-time requests reuse the last snapshot.
|
|||||||
|
|
||||||
- Linux: nl80211 via `github.com/mdlayher/wifi`. Docker needs `network_mode: host`.
|
- Linux: nl80211 via `github.com/mdlayher/wifi`. Docker needs `network_mode: host`.
|
||||||
- macOS: CoreWLAN via `osascript` (JXA). SSID may be redacted by privacy settings.
|
- macOS: CoreWLAN via `osascript` (JXA). SSID may be redacted by privacy settings.
|
||||||
- Windows: native WLAN API, keyed by interface GUID.
|
- Windows: native WLAN API, keyed by adapter alias (e.g. `Wi-Fi`), falling back
|
||||||
|
to interface GUID.
|
||||||
- Other platforms: unsupported.
|
- Other platforms: unsupported.
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ var wlanEnum = wlan.NewProc("WlanEnumInterfaces")
|
|||||||
var wlanQuery = wlan.NewProc("WlanQueryInterface")
|
var wlanQuery = wlan.NewProc("WlanQueryInterface")
|
||||||
var wlanFree = wlan.NewProc("WlanFreeMemory")
|
var wlanFree = wlan.NewProc("WlanFreeMemory")
|
||||||
|
|
||||||
|
var iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||||
|
var guidToLuid = iphlpapi.NewProc("ConvertInterfaceGuidToLuid")
|
||||||
|
var luidToAlias = iphlpapi.NewProc("ConvertInterfaceLuidToAlias")
|
||||||
|
|
||||||
type wlanInterface struct {
|
type wlanInterface struct {
|
||||||
GUID windows.GUID
|
GUID windows.GUID
|
||||||
Description [256]uint16
|
Description [256]uint16
|
||||||
@@ -84,7 +88,7 @@ func collect(ctx context.Context) map[string]system.WiFi {
|
|||||||
}
|
}
|
||||||
wlanFree.Call(uintptr(data))
|
wlanFree.Call(uintptr(data))
|
||||||
}
|
}
|
||||||
result[iface.GUID.String()] = reading
|
result[interfaceName(&iface.GUID)] = reading
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -97,3 +101,20 @@ func queryWLAN(handle windows.Handle, guid *windows.GUID, opcode uintptr) (unsaf
|
|||||||
}
|
}
|
||||||
return data, size
|
return data, size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interfaceName returns the adapter alias ("Wi-Fi"), matching the names used by
|
||||||
|
// network interface stats. Falls back to the GUID if the alias is unavailable.
|
||||||
|
func interfaceName(guid *windows.GUID) string {
|
||||||
|
if guidToLuid.Find() == nil && luidToAlias.Find() == nil {
|
||||||
|
var luid uint64
|
||||||
|
if rc, _, _ := guidToLuid.Call(uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(&luid))); rc == 0 {
|
||||||
|
var alias [windows.IF_MAX_STRING_SIZE + 1]uint16
|
||||||
|
if rc, _, _ := luidToAlias.Call(uintptr(unsafe.Pointer(&luid)), uintptr(unsafe.Pointer(&alias[0])), uintptr(len(alias))); rc == 0 {
|
||||||
|
if name := windows.UTF16ToString(alias[:]); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return guid.String()
|
||||||
|
}
|
||||||
|
|||||||
+77
-5
@@ -18,6 +18,11 @@ var (
|
|||||||
devZfsPath = "/dev/zfs"
|
devZfsPath = "/dev/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errNoPoolIOStats marks an "iostats" file that predates OpenZFS 2.3 and
|
||||||
|
// reports no pool read/write counters (TRIM statistics only), so callers may
|
||||||
|
// fall back to older counter sources.
|
||||||
|
var errNoPoolIOStats = errors.New("no pool I/O counters")
|
||||||
|
|
||||||
func ARCSize() (uint64, error) {
|
func ARCSize() (uint64, error) {
|
||||||
file, err := os.Open(filepath.Join(procZfsPath, "arcstats"))
|
file, err := os.Open(filepath.Join(procZfsPath, "arcstats"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,7 +66,7 @@ func checkZfsDevice() error {
|
|||||||
// ZFS collector and avoid keeping a `zpool iostat` subprocess alive.
|
// ZFS collector and avoid keeping a `zpool iostat` subprocess alive.
|
||||||
func PoolKernelStats() ([]PoolKernelStat, error) {
|
func PoolKernelStats() ([]PoolKernelStat, error) {
|
||||||
poolDirs := make(map[string]struct{})
|
poolDirs := make(map[string]struct{})
|
||||||
for _, filename := range []string{"state", "io", "objset-*"} {
|
for _, filename := range []string{"state", "io", "iostats", "objset-*"} {
|
||||||
paths, err := filepath.Glob(filepath.Join(procZfsPath, "*", filename))
|
paths, err := filepath.Glob(filepath.Join(procZfsPath, "*", filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -97,11 +102,25 @@ func PoolKernelStats() ([]PoolKernelStat, error) {
|
|||||||
return pools, nil
|
return pools, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPoolCounters supports both ZFS kernel interfaces. OpenZFS through 2.3
|
// readPoolCounters supports the ZFS kernel interfaces. OpenZFS 2.3+ exposes
|
||||||
// exposes aggregate vdev counters in "io". When that file is unavailable, sum
|
// pool-wide read/write counters in "iostats" that cover every objset,
|
||||||
// the logical I/O counters exposed for each dataset in the pool.
|
// including mounted snapshots, which never get an "objset-*" kstat. OpenZFS
|
||||||
|
// through 2.0 exposes aggregate vdev counters in "io". When neither pool-level
|
||||||
|
// interface is usable, sum the logical I/O counters exposed for each dataset.
|
||||||
func readPoolCounters(poolDir string) (uint64, uint64, error) {
|
func readPoolCounters(poolDir string) (uint64, uint64, error) {
|
||||||
nread, nwrite, err := readPoolIO(filepath.Join(poolDir, "io"))
|
nread, nwrite, err := readPoolIOStats(filepath.Join(poolDir, "iostats"))
|
||||||
|
if err == nil {
|
||||||
|
return nread, nwrite, nil
|
||||||
|
}
|
||||||
|
// Older sources are tried only when "iostats" is absent or predates
|
||||||
|
// OpenZFS 2.3. A read or parse failure on a usable file is returned:
|
||||||
|
// silently switching counter sources would feed kernelStats counters
|
||||||
|
// from different interfaces under the same pool name and produce a
|
||||||
|
// false I/O spike once "iostats" recovers.
|
||||||
|
if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, errNoPoolIOStats) {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
nread, nwrite, err = readPoolIO(filepath.Join(poolDir, "io"))
|
||||||
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||||
return nread, nwrite, err
|
return nread, nwrite, err
|
||||||
}
|
}
|
||||||
@@ -144,6 +163,59 @@ func readPoolIO(path string) (uint64, uint64, error) {
|
|||||||
return 0, 0, fmt.Errorf("I/O counters not found in %s", path)
|
return 0, 0, fmt.Errorf("I/O counters not found in %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readPoolIOStats reads the pool I/O counters exported by OpenZFS 2.3+ in the
|
||||||
|
// "iostats" kstat. Reads are counted on ARC misses at their compressed size, so
|
||||||
|
// reads served from the ARC are excluded; writes are counted at their logical
|
||||||
|
// size. The file exists on earlier releases but then only reports TRIM
|
||||||
|
// statistics, so all four byte counters are required for the file to be usable.
|
||||||
|
func readPoolIOStats(path string) (uint64, uint64, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var arcRead, arcWrite, directRead, directWrite uint64
|
||||||
|
var found uint8
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var target *uint64
|
||||||
|
var bit uint8
|
||||||
|
switch fields[0] {
|
||||||
|
case "arc_read_bytes":
|
||||||
|
target, bit = &arcRead, 1<<0
|
||||||
|
case "direct_read_bytes":
|
||||||
|
target, bit = &directRead, 1<<1
|
||||||
|
case "arc_write_bytes":
|
||||||
|
target, bit = &arcWrite, 1<<2
|
||||||
|
case "direct_write_bytes":
|
||||||
|
target, bit = &directWrite, 1<<3
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(fields[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("parsing %s in %s: %w", fields[0], path, err)
|
||||||
|
}
|
||||||
|
*target = value
|
||||||
|
found |= bit
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if found == 0 {
|
||||||
|
return 0, 0, fmt.Errorf("%w in %s", errNoPoolIOStats, path)
|
||||||
|
}
|
||||||
|
if found != 0x0f {
|
||||||
|
return 0, 0, fmt.Errorf("incomplete pool I/O counters in %s", path)
|
||||||
|
}
|
||||||
|
return arcRead + directRead, arcWrite + directWrite, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readPoolObjsets(poolDir string) (uint64, uint64, error) {
|
func readPoolObjsets(poolDir string) (uint64, uint64, error) {
|
||||||
paths, err := filepath.Glob(filepath.Join(poolDir, "objset-*"))
|
paths, err := filepath.Glob(filepath.Join(poolDir, "objset-*"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -66,6 +66,111 @@ func TestPoolKernelStatsOpenZfs24(t *testing.T) {
|
|||||||
}, stats[0])
|
}, stats[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPoolKernelStatsOpenZfs23(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
oldPath := procZfsPath
|
||||||
|
procZfsPath = root
|
||||||
|
t.Cleanup(func() { procZfsPath = oldPath })
|
||||||
|
|
||||||
|
// OpenZFS 2.3+ exposes logical pool read/write counters in "iostats".
|
||||||
|
// They cover every objset, including mounted snapshots, which never get
|
||||||
|
// an "objset-*" kstat, so the objset sum alone under-reports reads.
|
||||||
|
poolDir := filepath.Join(root, "tank")
|
||||||
|
require.NoError(t, os.MkdirAll(poolDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "state"), []byte("ONLINE\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "iostats"), []byte(
|
||||||
|
"15 1 0x01 26 5176 227423 2127661979232\n"+
|
||||||
|
"name type data\n"+
|
||||||
|
"trim_extents_written 4 0\n"+
|
||||||
|
"arc_read_count 4 12\n"+
|
||||||
|
"arc_read_bytes 4 5000\n"+
|
||||||
|
"arc_write_count 4 7\n"+
|
||||||
|
"arc_write_bytes 4 3000\n"+
|
||||||
|
"direct_read_count 4 1\n"+
|
||||||
|
"direct_read_bytes 4 500\n"+
|
||||||
|
"direct_write_count 4 1\n"+
|
||||||
|
"direct_write_bytes 4 200\n",
|
||||||
|
), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "objset-0x1"), []byte(
|
||||||
|
"34 1 0x01 28 7872 0 0\n"+
|
||||||
|
"name type data\n"+
|
||||||
|
"dataset_name 7 tank\n"+
|
||||||
|
"nwritten 4 2000\n"+
|
||||||
|
"nread 4 1000\n",
|
||||||
|
), 0o644))
|
||||||
|
|
||||||
|
stats, err := PoolKernelStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, stats, 1)
|
||||||
|
assert.Equal(t, PoolKernelStat{
|
||||||
|
Name: "tank", Health: "ONLINE", NRead: 5500, NWrite: 3200,
|
||||||
|
}, stats[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolKernelStatsIOStatsTrimOnly(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
oldPath := procZfsPath
|
||||||
|
procZfsPath = root
|
||||||
|
t.Cleanup(func() { procZfsPath = oldPath })
|
||||||
|
|
||||||
|
// Before OpenZFS 2.3 the "iostats" file only reports TRIM counters, so
|
||||||
|
// the per-dataset "objset-*" files remain the only usable source.
|
||||||
|
poolDir := filepath.Join(root, "tank")
|
||||||
|
require.NoError(t, os.MkdirAll(poolDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "state"), []byte("ONLINE\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "iostats"), []byte(
|
||||||
|
"15 1 0x01 18 3736 227423 2127661979232\n"+
|
||||||
|
"name type data\n"+
|
||||||
|
"trim_extents_written 4 10\n"+
|
||||||
|
"trim_bytes_written 4 4096\n",
|
||||||
|
), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "objset-0x1"), []byte(
|
||||||
|
"34 1 0x01 28 7872 0 0\n"+
|
||||||
|
"name type data\n"+
|
||||||
|
"dataset_name 7 tank\n"+
|
||||||
|
"nwritten 4 2000\n"+
|
||||||
|
"nread 4 1000\n",
|
||||||
|
), 0o644))
|
||||||
|
|
||||||
|
stats, err := PoolKernelStats()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, stats, 1)
|
||||||
|
assert.Equal(t, PoolKernelStat{
|
||||||
|
Name: "tank", Health: "ONLINE", NRead: 1000, NWrite: 2000,
|
||||||
|
}, stats[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolKernelStatsIOStatsErrorDoesNotFallback(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
oldPath := procZfsPath
|
||||||
|
procZfsPath = root
|
||||||
|
t.Cleanup(func() { procZfsPath = oldPath })
|
||||||
|
|
||||||
|
// A malformed "iostats" on a kernel that supports it must surface an
|
||||||
|
// error. Silently switching to the per-dataset sum would drop snapshot
|
||||||
|
// reads and, when "iostats" recovers, make kernelStats report a false
|
||||||
|
// I/O spike by comparing counters from two different interfaces.
|
||||||
|
poolDir := filepath.Join(root, "tank")
|
||||||
|
require.NoError(t, os.MkdirAll(poolDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "state"), []byte("ONLINE\n"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "iostats"), []byte(
|
||||||
|
"arc_read_bytes 4 notanumber\n"+
|
||||||
|
"arc_write_bytes 4 3000\n"+
|
||||||
|
"direct_read_bytes 4 500\n"+
|
||||||
|
"direct_write_bytes 4 200\n",
|
||||||
|
), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(poolDir, "objset-0x1"), []byte(
|
||||||
|
"34 1 0x01 28 7872 0 0\n"+
|
||||||
|
"name type data\n"+
|
||||||
|
"dataset_name 7 tank\n"+
|
||||||
|
"nwritten 4 2000\n"+
|
||||||
|
"nread 4 1000\n",
|
||||||
|
), 0o644))
|
||||||
|
|
||||||
|
_, err := PoolKernelStats()
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPoolKernelStatsNoZfs(t *testing.T) {
|
func TestPoolKernelStatsNoZfs(t *testing.T) {
|
||||||
oldPath := procZfsPath
|
oldPath := procZfsPath
|
||||||
procZfsPath = t.TempDir()
|
procZfsPath = t.TempDir()
|
||||||
@@ -89,6 +194,22 @@ func TestReadObjsetIORequiresAllCounters(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadPoolIOStatsRequiresAllCounters(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "iostats")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(
|
||||||
|
"arc_read_bytes 4 10\ndirect_read_bytes 4 5\narc_write_bytes 4 7\n"), 0o644))
|
||||||
|
_, _, err := readPoolIOStats(path)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPoolIOStatsTrimOnlySignalsFallback(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "iostats")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte(
|
||||||
|
"trim_extents_written 4 10\ntrim_bytes_written 4 4096\n"), 0o644))
|
||||||
|
_, _, err := readPoolIOStats(path)
|
||||||
|
assert.ErrorIs(t, err, errNoPoolIOStats)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCollectorsSkipCommandsWhenDevZfsMissing(t *testing.T) {
|
func TestCollectorsSkipCommandsWhenDevZfsMissing(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
oldDevZfsPath := devZfsPath
|
oldDevZfsPath := devZfsPath
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ type NVMeSmartHealthInformationLog struct {
|
|||||||
HostReads uint `json:"host_reads"`
|
HostReads uint `json:"host_reads"`
|
||||||
HostWrites uint `json:"host_writes"`
|
HostWrites uint `json:"host_writes"`
|
||||||
ControllerBusyTime uint `json:"controller_busy_time"`
|
ControllerBusyTime uint `json:"controller_busy_time"`
|
||||||
PowerCycles uint16 `json:"power_cycles"`
|
PowerCycles uint `json:"power_cycles"`
|
||||||
PowerOnHours uint32 `json:"power_on_hours"`
|
PowerOnHours uint32 `json:"power_on_hours"`
|
||||||
UnsafeShutdowns uint16 `json:"unsafe_shutdowns"`
|
UnsafeShutdowns uint16 `json:"unsafe_shutdowns"`
|
||||||
MediaErrors uint `json:"media_errors"`
|
MediaErrors uint `json:"media_errors"`
|
||||||
@@ -506,7 +506,7 @@ type SmartInfoForNvme struct {
|
|||||||
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
||||||
NVMeSmartHealthInformationLog *NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
NVMeSmartHealthInformationLog *NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
||||||
Temperature TemperatureInfoNvme `json:"temperature"`
|
Temperature TemperatureInfoNvme `json:"temperature"`
|
||||||
PowerCycleCount uint16 `json:"power_cycle_count"`
|
PowerCycleCount uint `json:"power_cycle_count"`
|
||||||
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { alertInfo } from "@/lib/alerts"
|
import { alertInfo } from "@/lib/alerts"
|
||||||
import { $alerts, $allSystemsById } from "@/lib/stores"
|
import { $alerts, $allSystemsById } from "@/lib/stores"
|
||||||
|
import { useBrowserStorage } from "@/lib/utils"
|
||||||
import type { AlertRecord } from "@/types"
|
import type { AlertRecord } from "@/types"
|
||||||
import { Plural, Trans } from "@lingui/react/macro"
|
import { Plural, Trans } from "@lingui/react/macro"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { AlertBannerSheet, AlertBannerSheetItem } from "./alert-banner-sheet"
|
import { AlertBannerSheet, AlertBannerSheetItem } from "./alert-banner-sheet"
|
||||||
import { $router } from "./router"
|
import { $router } from "./router"
|
||||||
|
|
||||||
@@ -49,6 +50,9 @@ export const ActiveAlerts = ({ className }: { className?: string }) => {
|
|||||||
const alerts = useStore($alerts)
|
const alerts = useStore($alerts)
|
||||||
const systems = useStore($allSystemsById)
|
const systems = useStore($allSystemsById)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
// ids of the alerts that were active when the banner was last dismissed.
|
||||||
|
// session storage because a retrigger while the page is closed keeps the same id.
|
||||||
|
const [dismissedIds, setDismissedIds] = useBrowserStorage<string[]>("dismissedAlerts", [], sessionStorage)
|
||||||
|
|
||||||
const { activeAlerts, systemCount, alertsKey } = useMemo(() => {
|
const { activeAlerts, systemCount, alertsKey } = useMemo(() => {
|
||||||
const activeAlerts: AlertRecord[] = []
|
const activeAlerts: AlertRecord[] = []
|
||||||
@@ -69,9 +73,24 @@ export const ActiveAlerts = ({ className }: { className?: string }) => {
|
|||||||
return { activeAlerts, systemCount: systemIds.size, alertsKey: alertsKey.join("") }
|
return { activeAlerts, systemCount: systemIds.size, alertsKey: alertsKey.join("") }
|
||||||
}, [alerts])
|
}, [alerts])
|
||||||
|
|
||||||
|
// forget dismissed alerts once they resolve so they show again if they retrigger.
|
||||||
|
// skipped while alerts are still loading so a reload doesn't clear the dismissal,
|
||||||
|
// and re-run once loaded in case they resolved while the page was closed.
|
||||||
|
const alertsLoaded = Object.keys(alerts).length > 0
|
||||||
|
useEffect(() => {
|
||||||
|
if (!alertsLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const activeIds = new Set(activeAlerts.map((alert) => alert.id))
|
||||||
|
if (dismissedIds.some((id) => !activeIds.has(id))) {
|
||||||
|
setDismissedIds(dismissedIds.filter((id) => activeIds.has(id)))
|
||||||
|
}
|
||||||
|
}, [alertsKey, alertsLoaded])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const alertCount = activeAlerts.length
|
const alertCount = activeAlerts.length
|
||||||
if (alertCount === 0) {
|
// stay hidden after dismissing until an alert triggers that wasn't active at the time
|
||||||
|
if (alertCount === 0 || activeAlerts.every((alert) => dismissedIds.includes(alert.id))) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// name the alert directly in the banner when there is only one
|
// name the alert directly in the banner when there is only one
|
||||||
@@ -80,6 +99,7 @@ export const ActiveAlerts = ({ className }: { className?: string }) => {
|
|||||||
<AlertBannerSheet
|
<AlertBannerSheet
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
|
onDismiss={() => setDismissedIds(activeAlerts.map((alert) => alert.id))}
|
||||||
className={className}
|
className={className}
|
||||||
title={
|
title={
|
||||||
alertCount === 1 ? (
|
alertCount === 1 ? (
|
||||||
@@ -117,5 +137,5 @@ export const ActiveAlerts = ({ className }: { className?: string }) => {
|
|||||||
})}
|
})}
|
||||||
</AlertBannerSheet>
|
</AlertBannerSheet>
|
||||||
)
|
)
|
||||||
}, [alertsKey, systemCount, systems, open, className])
|
}, [alertsKey, systemCount, systems, open, className, dismissedIds])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { ChevronRightIcon, type LucideIcon, TriangleAlertIcon } from "lucide-react"
|
import { ChevronRightIcon, type LucideIcon, TriangleAlertIcon, XIcon } from "lucide-react"
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Link } from "./router"
|
import { Link } from "./router"
|
||||||
@@ -20,6 +20,7 @@ export function AlertBannerSheet({
|
|||||||
sheetTitle,
|
sheetTitle,
|
||||||
sheetDescription,
|
sheetDescription,
|
||||||
icon: Icon = TriangleAlertIcon,
|
icon: Icon = TriangleAlertIcon,
|
||||||
|
onDismiss,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -31,9 +32,12 @@ export function AlertBannerSheet({
|
|||||||
sheetTitle: ReactNode
|
sheetTitle: ReactNode
|
||||||
sheetDescription?: ReactNode
|
sheetDescription?: ReactNode
|
||||||
icon?: LucideIcon
|
icon?: LucideIcon
|
||||||
|
/** Shows a dismiss button when provided */
|
||||||
|
onDismiss?: () => void
|
||||||
className?: string
|
className?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useLingui()
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<Alert variant="destructive" className={cn("flex items-center gap-3 py-3 max-sm:flex-wrap", className)}>
|
<Alert variant="destructive" className={cn("flex items-center gap-3 py-3 max-sm:flex-wrap", className)}>
|
||||||
@@ -42,7 +46,7 @@ export function AlertBannerSheet({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<AlertTitle className="m-0">{title}</AlertTitle>
|
<AlertTitle className="m-0">{title}</AlertTitle>
|
||||||
{description && (
|
{description && (
|
||||||
<AlertDescription className="text-destructive/80 dark:text-red-500/80">{description}</AlertDescription>
|
<AlertDescription className="text-destructive/80 dark:text-red-400/80">{description}</AlertDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,12 +54,24 @@ export function AlertBannerSheet({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="shrink-0 bg-transparent border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive dark:text-red-500 dark:hover:bg-destructive/15 dark:hover:text-red-500 max-sm:w-full"
|
className="shrink-0 bg-transparent border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive dark:text-red-400 dark:hover:bg-destructive/15 dark:hover:text-red-400 max-sm:w-full max-sm:order-last"
|
||||||
>
|
>
|
||||||
{buttonLabel ?? <Trans>View details</Trans>}
|
{buttonLabel ?? <Trans>View details</Trans>}
|
||||||
<ChevronRightIcon className="size-4 ms-1 -me-1" />
|
<ChevronRightIcon className="size-4 ms-1 -me-1" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
|
{onDismiss && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onDismiss}
|
||||||
|
aria-label={t`Dismiss`}
|
||||||
|
title={t`Dismiss`}
|
||||||
|
className="size-8 shrink-0 -me-1.5 text-destructive/70 hover:bg-destructive/10 hover:text-destructive dark:text-red-400/70 dark:hover:bg-destructive/15 dark:hover:text-red-400"
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
<SheetContent className="w-140 !max-w-full gap-0">
|
<SheetContent className="w-140 !max-w-full gap-0">
|
||||||
<SheetHeader className="p-4 sm:p-6 pb-3 sm:pb-4 border-b">
|
<SheetHeader className="p-4 sm:p-6 pb-3 sm:pb-4 border-b">
|
||||||
@@ -88,7 +104,7 @@ export function AlertBannerSheetItem({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="group flex items-start gap-3 rounded-lg border p-3 transition-colors hover:bg-accent/60"
|
className="group flex items-start gap-3 rounded-lg border p-3 transition-colors hover:bg-accent/60"
|
||||||
>
|
>
|
||||||
<div className="rounded-md bg-destructive/10 p-2 text-destructive shrink-0">
|
<div className="rounded-md bg-destructive/10 p-2 text-destructive dark:text-red-400 shrink-0">
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
|||||||
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
@@ -17,7 +18,7 @@ import { alertInfo } from "@/lib/alerts"
|
|||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
import { $alerts, $systems } from "@/lib/stores"
|
import { $alerts, $systems } from "@/lib/stores"
|
||||||
import { cn, debounce } from "@/lib/utils"
|
import { cn, debounce } from "@/lib/utils"
|
||||||
import type { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
import type { AlertInfo, AlertRecord, AlertUnit, SystemRecord } from "@/types"
|
||||||
|
|
||||||
const Slider = lazy(() => import("@/components/ui/slider"))
|
const Slider = lazy(() => import("@/components/ui/slider"))
|
||||||
|
|
||||||
@@ -27,6 +28,24 @@ const alertDebounce = 400
|
|||||||
|
|
||||||
const alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]
|
const alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]
|
||||||
|
|
||||||
|
/** Round to a multiple of step, trimming float noise like 0.30000000000000004 */
|
||||||
|
const roundToStep = (val: number, step: number) => Number((Math.round(val / step) * step).toFixed(6))
|
||||||
|
|
||||||
|
/** Number input step and minimum for a unit, which may be finer than the slider */
|
||||||
|
const getInputRange = ({ min, step, inputStep = step }: AlertUnit) => ({ step: inputStep, min: Math.min(min, inputStep) })
|
||||||
|
|
||||||
|
/** Index of the unit to display a stored value in: the largest unit whose input range and step fit it */
|
||||||
|
function getUnitIndex(units: AlertUnit[], value: number) {
|
||||||
|
for (let i = units.length - 1; i > 0; i--) {
|
||||||
|
const { min, step } = getInputRange(units[i])
|
||||||
|
const val = value / units[i].factor
|
||||||
|
if (val >= min && val <= units[i].max && Math.abs(roundToStep(val, step) - val) < 1e-6) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const failedUpdateToast = (error: unknown) => {
|
const failedUpdateToast = (error: unknown) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
toast({
|
toast({
|
||||||
@@ -245,7 +264,17 @@ export function AlertContent({
|
|||||||
|
|
||||||
const [checked, setChecked] = useState(global ? false : !!alert)
|
const [checked, setChecked] = useState(global ? false : !!alert)
|
||||||
const [min, setMin] = useState(alert?.min || (noDuration ? 0 : 10))
|
const [min, setMin] = useState(alert?.min || (noDuration ? 0 : 10))
|
||||||
const [value, setValue] = useState(alert?.value ?? (noThreshold ? 0 : (alertData.start ?? 80)))
|
const { units } = alertData
|
||||||
|
const storedValue = alert?.value ?? (noThreshold ? 0 : (alertData.start ?? 80))
|
||||||
|
const [unitIndex, setUnitIndex] = useState(() => (units ? getUnitIndex(units, storedValue) : 0))
|
||||||
|
/** Selected threshold unit, if the alert has selectable units */
|
||||||
|
const unit = units?.[unitIndex]
|
||||||
|
const factor = unit?.factor ?? 1
|
||||||
|
const unitLabel = unit?.unit ?? alertData.unit
|
||||||
|
const { min: valueMin, max: valueMax, step = 1 } = unit ?? alertData
|
||||||
|
const { min: inputMin, step: inputStep } = unit ? getInputRange(unit) : { min: valueMin, step }
|
||||||
|
/** Threshold value in the selected unit */
|
||||||
|
const [value, setValue] = useState(() => Number((storedValue / factor).toFixed(6)))
|
||||||
|
|
||||||
const Icon = alertData.icon
|
const Icon = alertData.icon
|
||||||
|
|
||||||
@@ -267,12 +296,12 @@ export function AlertContent({
|
|||||||
return systemIds
|
return systemIds
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendUpsert(min: number, value: number) {
|
function sendUpsert(min: number, value: number, valueFactor = factor) {
|
||||||
const systems = getSystemIds()
|
const systems = getSystemIds()
|
||||||
systems.length &&
|
systems.length &&
|
||||||
upsertAlerts({
|
upsertAlerts({
|
||||||
name: alertKey,
|
name: alertKey,
|
||||||
value,
|
value: Number((value * valueFactor).toFixed(6)),
|
||||||
min,
|
min,
|
||||||
systems,
|
systems,
|
||||||
})
|
})
|
||||||
@@ -326,7 +355,7 @@ export function AlertContent({
|
|||||||
Average drops below{" "}
|
Average drops below{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{alertData.unit}
|
{unitLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
@@ -334,7 +363,7 @@ export function AlertContent({
|
|||||||
Average exceeds{" "}
|
Average exceeds{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{alertData.unit}
|
{unitLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
@@ -345,9 +374,9 @@ export function AlertContent({
|
|||||||
value={[value]}
|
value={[value]}
|
||||||
onValueCommit={(val) => sendUpsert(min, val[0])}
|
onValueCommit={(val) => sendUpsert(min, val[0])}
|
||||||
onValueChange={(val) => setValue(val[0])}
|
onValueChange={(val) => setValue(val[0])}
|
||||||
step={alertData.step ?? 1}
|
step={step}
|
||||||
min={alertData.min ?? 1}
|
min={valueMin ?? 1}
|
||||||
max={alertData.max ?? 99}
|
max={valueMax ?? 99}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -355,17 +384,45 @@ export function AlertContent({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let val = parseFloat(e.target.value)
|
let val = parseFloat(e.target.value)
|
||||||
if (!Number.isNaN(val)) {
|
if (!Number.isNaN(val)) {
|
||||||
if (alertData.max != null) val = Math.min(val, alertData.max)
|
// keep values on the step grid so the unit can be inferred on reload
|
||||||
if (alertData.min != null) val = Math.max(val, alertData.min)
|
if (unit) val = roundToStep(val, inputStep)
|
||||||
|
if (valueMax != null) val = Math.min(val, valueMax)
|
||||||
|
if (inputMin != null) val = Math.max(val, inputMin)
|
||||||
setValue(val)
|
setValue(val)
|
||||||
sendUpsert(min, val)
|
sendUpsert(min, val)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
step={alertData.step ?? 1}
|
step={inputStep}
|
||||||
min={alertData.min ?? 1}
|
min={inputMin ?? 1}
|
||||||
max={alertData.max ?? 99}
|
max={valueMax ?? 99}
|
||||||
className="w-16 h-8 text-center px-1"
|
className="w-16 h-8 text-center px-1"
|
||||||
/>
|
/>
|
||||||
|
{units && (
|
||||||
|
<Select
|
||||||
|
value={String(unitIndex)}
|
||||||
|
onValueChange={(index) => {
|
||||||
|
const next = units[Number(index)]
|
||||||
|
const nextInput = getInputRange(next)
|
||||||
|
// convert the current threshold to the new unit, clamped to its range
|
||||||
|
const val = roundToStep((value * factor) / next.factor, nextInput.step)
|
||||||
|
const clamped = Math.min(Math.max(val, nextInput.min), next.max)
|
||||||
|
setUnitIndex(Number(index))
|
||||||
|
setValue(clamped)
|
||||||
|
sendUpsert(min, clamped, next.factor)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-auto shrink-0 h-8 gap-1.5 px-2.5">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{units.map(({ unit }, i) => (
|
||||||
|
<SelectItem key={unit} value={String(i)}>
|
||||||
|
{unit.trim()}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import { Separator } from "../ui/separator"
|
|||||||
import { $router, Link } from "../router"
|
import { $router, Link } from "../router"
|
||||||
import { listenKeys } from "nanostores"
|
import { listenKeys } from "nanostores"
|
||||||
import { getPagePath } from "@nanostores/router"
|
import { getPagePath } from "@nanostores/router"
|
||||||
import { LogsDisplay, LogsFullscreenDialog } from "@/components/logs-display"
|
import { LogsDisplay, LogsFullscreenDialog, LogsIconButton, LogsTimestampToggle } from "@/components/logs-display"
|
||||||
|
import { getLogTimestampDecorations } from "@/lib/logs"
|
||||||
|
|
||||||
const syntaxTheme = "github-dark-dimmed"
|
const syntaxTheme = "github-dark-dimmed"
|
||||||
|
|
||||||
@@ -282,7 +283,13 @@ async function getLogsHtml(container: ContainerRecord): Promise<string> {
|
|||||||
container: container.id,
|
container: container.id,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
return logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: "log", theme: syntaxTheme }) : t`No results.`
|
return logsHtml.logs
|
||||||
|
? highlighter.codeToHtml(logsHtml.logs, {
|
||||||
|
lang: "log",
|
||||||
|
theme: syntaxTheme,
|
||||||
|
decorations: getLogTimestampDecorations(logsHtml.logs),
|
||||||
|
})
|
||||||
|
: t`No results.`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return ""
|
return ""
|
||||||
@@ -410,20 +417,15 @@ function ContainerSheet({
|
|||||||
<div className="px-3 pb-3 -mt-4 flex flex-col gap-3 h-full items-start">
|
<div className="px-3 pb-3 -mt-4 flex flex-col gap-3 h-full items-start">
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<h3>{t`Logs`}</h3>
|
<h3>{t`Logs`}</h3>
|
||||||
<Button
|
<LogsTimestampToggle className="ms-auto" />
|
||||||
variant="ghost"
|
<LogsIconButton label={t`Refresh`} onClick={refreshLogs} disabled={isRefreshingLogs}>
|
||||||
size="sm"
|
|
||||||
onClick={refreshLogs}
|
|
||||||
className="h-8 w-8 p-0 ms-auto"
|
|
||||||
disabled={isRefreshingLogs}
|
|
||||||
>
|
|
||||||
<RefreshCwIcon
|
<RefreshCwIcon
|
||||||
className={`size-4 transition-transform duration-300 ${isRefreshingLogs ? "animate-spin" : ""}`}
|
className={`size-4 transition-transform duration-300 ${isRefreshingLogs ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</LogsIconButton>
|
||||||
<Button variant="ghost" size="sm" onClick={() => setLogsFullscreenOpen(true)} className="h-8 w-8 p-0">
|
<LogsIconButton label={t`Fullscreen`} onClick={() => setLogsFullscreenOpen(true)}>
|
||||||
<MaximizeIcon className="size-4" />
|
<MaximizeIcon className="size-4" />
|
||||||
</Button>
|
</LogsIconButton>
|
||||||
</div>
|
</div>
|
||||||
<LogsDisplay logsDisplay={logsDisplay} containerRef={logsContainerRef} />
|
<LogsDisplay logsDisplay={logsDisplay} containerRef={logsContainerRef} />
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: log HTML is generated locally by Shiki */
|
/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: log HTML is generated locally by Shiki */
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import { RefreshCwIcon } from "lucide-react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { type RefObject, useEffect, useRef } from "react"
|
import { ClockIcon, RefreshCwIcon } from "lucide-react"
|
||||||
|
import { type ComponentProps, type FocusEvent, type RefObject, useEffect, useRef } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import { Dialog, DialogContent, DialogTitle, dialogIconButtonClassName } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogTitle, dialogIconButtonClassName } from "@/components/ui/dialog"
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
|
import { $showLogTimestamps, toggleLogTimestamps } from "@/lib/stores"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
type LogsDisplayProps = {
|
type LogsDisplayProps = {
|
||||||
@@ -10,13 +14,51 @@ type LogsDisplayProps = {
|
|||||||
containerRef: RefObject<HTMLDivElement | null>
|
containerRef: RefObject<HTMLDivElement | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Radix opens a tooltip on any focus, including when a sheet or dialog moves focus to
|
||||||
|
// the button on open / close. Limit it to keyboard focus.
|
||||||
|
function keyboardFocusOnly(e: FocusEvent<HTMLButtonElement>) {
|
||||||
|
if (!e.currentTarget.matches(":focus-visible")) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon button with a tooltip, used in the log panel headers.
|
||||||
|
export function LogsIconButton({ label, className, ...props }: { label: string } & ComponentProps<typeof Button>) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild onFocus={keyboardFocusOnly}>
|
||||||
|
<Button variant="ghost" size="sm" aria-label={label} className={cn("h-8 w-8 p-0", className)} {...props} />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp visibility is a single persisted preference shared by all log views.
|
||||||
|
export function LogsTimestampToggle({ className }: { className?: string }) {
|
||||||
|
const showTimestamps = useStore($showLogTimestamps)
|
||||||
|
return (
|
||||||
|
<LogsIconButton
|
||||||
|
label={showTimestamps ? t`Hide timestamps` : t`Show timestamps`}
|
||||||
|
onClick={toggleLogTimestamps}
|
||||||
|
className={cn(showTimestamps && "bg-accent text-accent-foreground", className)}
|
||||||
|
aria-label={t`Show timestamps`}
|
||||||
|
aria-pressed={showTimestamps}
|
||||||
|
>
|
||||||
|
<ClockIcon className="size-4" />
|
||||||
|
</LogsIconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Shared by Docker and systemd service sheets so logs behave identically.
|
// Shared by Docker and systemd service sheets so logs behave identically.
|
||||||
export function LogsDisplay({ logsDisplay, containerRef }: LogsDisplayProps) {
|
export function LogsDisplay({ logsDisplay, containerRef }: LogsDisplayProps) {
|
||||||
|
const showTimestamps = useStore($showLogTimestamps)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm",
|
"max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm",
|
||||||
|
!showTimestamps && "[&_.log-timestamp]:hidden",
|
||||||
!logsDisplay && ["animate-pulse", "h-full"]
|
!logsDisplay && ["animate-pulse", "h-full"]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -40,6 +82,7 @@ export function LogsFullscreenDialog({
|
|||||||
onRefresh: () => void | Promise<void>
|
onRefresh: () => void | Promise<void>
|
||||||
isRefreshing: boolean
|
isRefreshing: boolean
|
||||||
}) {
|
}) {
|
||||||
|
const showTimestamps = useStore($showLogTimestamps)
|
||||||
const outerContainerRef = useRef<HTMLDivElement>(null)
|
const outerContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,19 +100,31 @@ export function LogsFullscreenDialog({
|
|||||||
<DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white">
|
<DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white">
|
||||||
<DialogTitle className="sr-only">{name} logs</DialogTitle>
|
<DialogTitle className="sr-only">{name} logs</DialogTitle>
|
||||||
<div ref={outerContainerRef} className="absolute inset-x-0 bottom-0 top-12 overflow-auto">
|
<div ref={outerContainerRef} className="absolute inset-x-0 bottom-0 top-12 overflow-auto">
|
||||||
<div className="min-h-full w-full px-3 leading-relaxed rounded-md bg-gh-dark text-sm">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"min-h-full w-full px-3 leading-relaxed rounded-md bg-gh-dark text-sm",
|
||||||
|
!showTimestamps && "[&_.log-timestamp]:hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="py-3" dangerouslySetInnerHTML={{ __html: logsDisplay }} />
|
<div className="py-3" dangerouslySetInnerHTML={{ __html: logsDisplay }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<LogsTimestampToggle className="absolute end-18 top-2 hover:bg-white/10 hover:text-white aria-pressed:bg-white/15 aria-pressed:text-white" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild onFocus={keyboardFocusOnly}>
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
className={cn("absolute end-11 top-3 opacity-60 hover:opacity-100", dialogIconButtonClassName)}
|
className={cn("absolute end-11 top-3 opacity-60 hover:opacity-100", dialogIconButtonClassName)}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
title={t`Refresh`}
|
|
||||||
aria-label={t`Refresh`}
|
aria-label={t`Refresh`}
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className={cn("size-4 transition-transform duration-300", isRefreshing && "animate-spin")} />
|
<RefreshCwIcon
|
||||||
|
className={cn("size-4 transition-transform duration-300", isRefreshing && "animate-spin")}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t`Refresh`}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Trans, useLingui } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
@@ -23,10 +23,11 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { ChevronDownIcon, ListIcon, PlusIcon, SearchIcon, ServerIcon } from "lucide-react"
|
import { ChevronDownIcon, GlobeIcon, ListIcon, type LucideIcon, PlusIcon, SearchIcon, ServerIcon } from "lucide-react"
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
import { $systems } from "@/lib/stores"
|
import { $systems } from "@/lib/stores"
|
||||||
import { cn, supportsNetworkMonitors } from "@/lib/utils"
|
import { cn, supportsNetworkMonitors } from "@/lib/utils"
|
||||||
|
import { getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||||
import type { NetworkMonitorRecord } from "@/types"
|
import type { NetworkMonitorRecord } from "@/types"
|
||||||
import * as v from "valibot"
|
import * as v from "valibot"
|
||||||
|
|
||||||
@@ -94,9 +95,22 @@ const NormalizedMonitorValuesSchema = v.pipe(
|
|||||||
return Number.isInteger(input.port) && input.port >= 1 && input.port <= 65535
|
return Number.isInteger(input.port) && input.port >= 1 && input.port <= 65535
|
||||||
}, "Port must be between 1 and 65535"),
|
}, "Port must be between 1 and 65535"),
|
||||||
["port"]
|
["port"]
|
||||||
|
),
|
||||||
|
// Resolving an IP literal returns it without querying anything, so the check would measure nothing.
|
||||||
|
v.forward(
|
||||||
|
v.check(
|
||||||
|
(input) => input.protocol !== "dns" || !isIpAddress(input.target),
|
||||||
|
"DNS target must be a domain name; put the resolver's IP in DNS Server"
|
||||||
|
),
|
||||||
|
["target"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function isIpAddress(value: string) {
|
||||||
|
// Hostnames never contain ":", so any colon means an IPv6 literal (optionally bracketed).
|
||||||
|
return /^(\d{1,3}\.){3}\d{1,3}$/.test(value) || value.includes(":")
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk parsing only trims raw CSV fields. Inference, defaults, and protocol-
|
// Bulk parsing only trims raw CSV fields. Inference, defaults, and protocol-
|
||||||
// specific validation still go through the shared normalization schema above.
|
// specific validation still go through the shared normalization schema above.
|
||||||
const BulkMonitorSchema = v.object({
|
const BulkMonitorSchema = v.object({
|
||||||
@@ -202,6 +216,7 @@ export function SystemMultiSelect({
|
|||||||
className,
|
className,
|
||||||
systemIds,
|
systemIds,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
canSelectMore,
|
||||||
}: {
|
}: {
|
||||||
id: string
|
id: string
|
||||||
selectedSystemIds: Set<string>
|
selectedSystemIds: Set<string>
|
||||||
@@ -211,8 +226,107 @@ export function SystemMultiSelect({
|
|||||||
/** Limit the options to these systems. Defaults to all systems that support network monitors. */
|
/** Limit the options to these systems. Defaults to all systems that support network monitors. */
|
||||||
systemIds?: string[]
|
systemIds?: string[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
canSelectMore?: boolean
|
||||||
}) {
|
}) {
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
|
const { t } = useLingui()
|
||||||
|
const options = systems
|
||||||
|
.filter((system) => (systemIds ? systemIds.includes(system.id) : supportsNetworkMonitors(system)))
|
||||||
|
.map((system) => ({ id: system.id, label: system.name }))
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
id={id}
|
||||||
|
options={options}
|
||||||
|
selectedIds={selectedSystemIds}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
className={className}
|
||||||
|
icon={ServerIcon}
|
||||||
|
canSelectMore={canSelectMore}
|
||||||
|
placeholder={placeholder ?? t`Select systems`}
|
||||||
|
searchPlaceholder={t`Search systems`}
|
||||||
|
emptyText={<Trans>No systems found.</Trans>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pick monitors by target, e.g. other targets on the same system to compare against. */
|
||||||
|
export function MonitorMultiSelect({
|
||||||
|
id,
|
||||||
|
monitors,
|
||||||
|
selectedMonitorIds,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
canSelectMore,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
monitors: NetworkMonitorRecord[]
|
||||||
|
selectedMonitorIds: Set<string>
|
||||||
|
onChange: (ids: Set<string>) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
placeholder?: string
|
||||||
|
canSelectMore?: boolean
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui()
|
||||||
|
const options = monitors
|
||||||
|
.map((monitor) => ({ id: monitor.id, label: getMonitorTarget(monitor), server: monitor.server }))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
id={id}
|
||||||
|
options={options}
|
||||||
|
selectedIds={selectedMonitorIds}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn("ps-9.5", className)}
|
||||||
|
icon={GlobeIcon}
|
||||||
|
canSelectMore={canSelectMore}
|
||||||
|
placeholder={placeholder ?? t`Select targets`}
|
||||||
|
searchPlaceholder={t`Search targets`}
|
||||||
|
emptyText={<Trans>No targets found.</Trans>}
|
||||||
|
renderOption={(option) => (
|
||||||
|
<>
|
||||||
|
<span className="truncate">{option.label}</span>
|
||||||
|
{option.server && <span className="ms-auto shrink-0 text-xs text-muted-foreground">{option.server}</span>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiSelectOption = { id: string; label: string }
|
||||||
|
|
||||||
|
function MultiSelect<T extends MultiSelectOption>({
|
||||||
|
id,
|
||||||
|
options,
|
||||||
|
selectedIds,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
icon: Icon,
|
||||||
|
placeholder,
|
||||||
|
searchPlaceholder,
|
||||||
|
emptyText,
|
||||||
|
renderOption = (option) => <span className="truncate">{option.label}</span>,
|
||||||
|
canSelectMore = true,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
options: T[]
|
||||||
|
selectedIds: Set<string>
|
||||||
|
onChange: (ids: Set<string>) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
icon: LucideIcon
|
||||||
|
placeholder: string
|
||||||
|
searchPlaceholder: string
|
||||||
|
emptyText: ReactNode
|
||||||
|
renderOption?: (option: T) => ReactNode
|
||||||
|
/** False once the selection is full; only already selected options can then be toggled. */
|
||||||
|
canSelectMore?: boolean
|
||||||
|
}) {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const searchRef = useRef<HTMLInputElement>(null)
|
const searchRef = useRef<HTMLInputElement>(null)
|
||||||
@@ -225,19 +339,15 @@ export function SystemMultiSelect({
|
|||||||
}, [])
|
}, [])
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const query = search.trim().toLocaleLowerCase()
|
const query = search.trim().toLocaleLowerCase()
|
||||||
const filteredSystems = systems.filter(
|
const filteredOptions = options.filter((option) => option.label.toLocaleLowerCase().includes(query))
|
||||||
(system) =>
|
const allSelected = filteredOptions.every((option) => selectedIds.has(option.id))
|
||||||
(systemIds ? systemIds.includes(system.id) : supportsNetworkMonitors(system)) &&
|
const anySelected = filteredOptions.some((option) => selectedIds.has(option.id))
|
||||||
system.name.toLocaleLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
const allSelected = filteredSystems.every((system) => selectedSystemIds.has(system.id))
|
|
||||||
const anySelected = filteredSystems.some((system) => selectedSystemIds.has(system.id))
|
|
||||||
|
|
||||||
const selectFiltered = (selected: boolean) => {
|
const selectFiltered = (selected: boolean) => {
|
||||||
const next = new Set(selectedSystemIds)
|
const next = new Set(selectedIds)
|
||||||
for (const system of filteredSystems) {
|
for (const option of filteredOptions) {
|
||||||
if (selected) next.add(system.id)
|
if (selected) next.add(option.id)
|
||||||
else next.delete(system.id)
|
else next.delete(option.id)
|
||||||
}
|
}
|
||||||
onChange(next)
|
onChange(next)
|
||||||
}
|
}
|
||||||
@@ -251,13 +361,13 @@ export function SystemMultiSelect({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn("relative w-full min-w-0 ps-10 pe-10 justify-start font-normal text-start", className)}
|
className={cn("relative w-full min-w-0 ps-10 pe-10 justify-start font-normal text-start", className)}
|
||||||
>
|
>
|
||||||
<ServerIcon className="size-3.5 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
|
<Icon className="size-3.5 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{selectedSystemIds.size === 0
|
{selectedIds.size === 0
|
||||||
? (placeholder ?? t`Select systems`)
|
? placeholder
|
||||||
: selectedSystemIds.size === 1
|
: selectedIds.size === 1
|
||||||
? systems.find((s) => selectedSystemIds.has(s.id))?.name
|
? options.find((option) => selectedIds.has(option.id))?.label
|
||||||
: t`${selectedSystemIds.size} selected`}
|
: t`${selectedIds.size} selected`}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon className="size-4 absolute end-4 top-1/2 -translate-y-1/2 opacity-50" />
|
<ChevronDownIcon className="size-4 absolute end-4 top-1/2 -translate-y-1/2 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -280,8 +390,8 @@ export function SystemMultiSelect({
|
|||||||
ref={focusSearchOnMount}
|
ref={focusSearchOnMount}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
placeholder={t`Search systems`}
|
placeholder={searchPlaceholder}
|
||||||
aria-label={t`Search systems`}
|
aria-label={searchPlaceholder}
|
||||||
className="h-10 min-w-0 rounded-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
className="h-10 min-w-0 rounded-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Escape") return
|
if (event.key === "Escape") return
|
||||||
@@ -303,7 +413,7 @@ export function SystemMultiSelect({
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="px-1.5 py-1 text-xs text-muted-foreground"
|
className="px-1.5 py-1 text-xs text-muted-foreground"
|
||||||
disabled={!filteredSystems.length || allSelected}
|
disabled={!filteredOptions.length || allSelected || !canSelectMore}
|
||||||
onSelect={(event) => {
|
onSelect={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
selectFiltered(true)
|
selectFiltered(true)
|
||||||
@@ -325,32 +435,29 @@ export function SystemMultiSelect({
|
|||||||
{query ? <Trans>Clear matches</Trans> : <Trans>Clear all</Trans>}
|
{query ? <Trans>Clear matches</Trans> : <Trans>Clear all</Trans>}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</div>
|
</div>
|
||||||
<span className="px-1.5 text-xs tabular-nums text-muted-foreground">
|
<span className="px-1.5 text-xs tabular-nums text-muted-foreground">{t`${selectedIds.size} selected`}</span>
|
||||||
{t`${selectedSystemIds.size} selected`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 overflow-y-auto">
|
<div className="min-h-0 overflow-y-auto">
|
||||||
{filteredSystems.length === 0 && (
|
{filteredOptions.length === 0 && (
|
||||||
<output className="block px-2.5 py-3 text-sm text-muted-foreground">
|
<output className="block px-2.5 py-3 text-sm text-muted-foreground">{emptyText}</output>
|
||||||
<Trans>No systems found.</Trans>
|
|
||||||
</output>
|
|
||||||
)}
|
)}
|
||||||
{filteredSystems.map((sys) => (
|
{filteredOptions.map((option) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={sys.id}
|
key={option.id}
|
||||||
checked={selectedSystemIds.has(sys.id)}
|
checked={selectedIds.has(option.id)}
|
||||||
|
disabled={!canSelectMore && !selectedIds.has(option.id)}
|
||||||
onSelect={(event) => event.preventDefault()}
|
onSelect={(event) => event.preventDefault()}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
const next = new Set(selectedSystemIds)
|
const next = new Set(selectedIds)
|
||||||
if (checked) next.add(sys.id)
|
if (checked) next.add(option.id)
|
||||||
else next.delete(sys.id)
|
else next.delete(option.id)
|
||||||
onChange(next)
|
onChange(next)
|
||||||
}}
|
}}
|
||||||
className="group min-w-0 gap-2.5 py-2 ps-2.5"
|
className="group min-w-0 gap-2.5 py-2 ps-2.5"
|
||||||
indicatorClassName="static size-4 shrink-0 rounded border border-input group-data-[state=checked]:border-primary group-data-[state=checked]:bg-primary group-data-[state=checked]:text-primary-foreground [&_svg]:size-3"
|
indicatorClassName="static size-4 shrink-0 rounded border border-input group-data-[state=checked]:border-primary group-data-[state=checked]:bg-primary group-data-[state=checked]:text-primary-foreground [&_svg]:size-3"
|
||||||
>
|
>
|
||||||
<span className="truncate">{sys.name}</span>
|
{renderOption(option)}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -620,6 +727,7 @@ function MonitorDialogContent({
|
|||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const isEditing = !!monitor
|
const isEditing = !!monitor
|
||||||
|
const dnsTargetIsIp = protocol === "dns" && isIpAddress(target.trim())
|
||||||
|
|
||||||
// When the dialog is opened, initialize form fields with monitor values (if editing) or defaults (if adding).
|
// When the dialog is opened, initialize form fields with monitor values (if editing) or defaults (if adding).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -733,8 +841,14 @@ function MonitorDialogContent({
|
|||||||
value={target}
|
value={target}
|
||||||
onChange={(e) => setTarget(e.target.value)}
|
onChange={(e) => setTarget(e.target.value)}
|
||||||
placeholder={protocol === "http" ? "http://localhost:8090" : protocol === "dns" ? "example.com" : "1.1.1.1"}
|
placeholder={protocol === "http" ? "http://localhost:8090" : protocol === "dns" ? "example.com" : "1.1.1.1"}
|
||||||
|
aria-invalid={dnsTargetIsIp}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
{dnsTargetIsIp && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
<Trans>Enter a domain name to look up. Put the resolver's IP in DNS Server.</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
@@ -811,7 +925,9 @@ function MonitorDialogContent({
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || (!systemId && (isEditing ? !selectedSystemId : !selectedSystemIds.size))}
|
disabled={
|
||||||
|
loading || dnsTargetIsIp || (!systemId && (isEditing ? !selectedSystemId : !selectedSystemIds.size))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Trans>Save {{ foo: t`Monitor` }}</Trans>
|
<Trans>Save {{ foo: t`Monitor` }}</Trans>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
CopyPlusIcon,
|
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
@@ -25,9 +24,6 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Plural, Trans } from "@lingui/react/macro"
|
import { Plural, Trans } from "@lingui/react/macro"
|
||||||
@@ -40,7 +36,6 @@ import { useMemo } from "react"
|
|||||||
import { formatBulkMonitorLine } from "@/components/network-monitors-table/monitor-dialog"
|
import { formatBulkMonitorLine } from "@/components/network-monitors-table/monitor-dialog"
|
||||||
import { Badge } from "../ui/badge"
|
import { Badge } from "../ui/badge"
|
||||||
import { getCertDaysLeft, getCertExpiryLevel, getMonitorTarget } from "@/lib/network-monitor-utils"
|
import { getCertDaysLeft, getCertExpiryLevel, getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||||
import { pb } from "@/lib/api"
|
|
||||||
|
|
||||||
const certExpiryDotColors = { ok: "bg-green-500", warning: "bg-yellow-500", critical: "bg-red-500" }
|
const certExpiryDotColors = { ok: "bg-green-500", warning: "bg-yellow-500", critical: "bg-red-500" }
|
||||||
|
|
||||||
@@ -308,11 +303,6 @@ export function getMonitorColumns(
|
|||||||
const isBulkAction = actionRows.length > 1
|
const isBulkAction = actionRows.length > 1
|
||||||
const shouldPause = actionRows.some((monitor) => monitor.enabled)
|
const shouldPause = actionRows.some((monitor) => monitor.enabled)
|
||||||
const bulkCopyContent = actionRows.map((monitor) => formatBulkMonitorLine(monitor)).join("\n")
|
const bulkCopyContent = actionRows.map((monitor) => formatBulkMonitorLine(monitor)).join("\n")
|
||||||
const allSystems = useStore($allSystemsById)
|
|
||||||
const otherSystems = useMemo(
|
|
||||||
() => Object.values(allSystems).filter((s) => !isBulkAction && s.id !== row.original.system),
|
|
||||||
[allSystems, isBulkAction]
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -357,31 +347,8 @@ export function getMonitorColumns(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="me-2.5 size-4" />
|
<CopyIcon className="me-2.5 size-4" />
|
||||||
<Trans>Bulk copy</Trans>
|
<Trans>Copy bulk config</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{!isBulkAction && otherSystems.length > 0 && (
|
|
||||||
<DropdownMenuSub>
|
|
||||||
<DropdownMenuSubTrigger>
|
|
||||||
<CopyPlusIcon className="me-2.5 size-4" />
|
|
||||||
<Trans>Copy to system</Trans>
|
|
||||||
</DropdownMenuSubTrigger>
|
|
||||||
<DropdownMenuSubContent className="max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] overflow-y-auto">
|
|
||||||
{otherSystems.map((sys) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={sys.id}
|
|
||||||
onClick={() => {
|
|
||||||
const { id: _id, system: _system, ...rest } = row.original
|
|
||||||
pb.collection("network_monitors")
|
|
||||||
.create({ ...rest, system: sys.id })
|
|
||||||
.catch(() => {})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sys.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { SystemStatus } from "@/lib/enums"
|
|||||||
import { $allSystemsById, $direction, $textMeasureVersion, $userSettings, getUserChartTime } from "@/lib/stores"
|
import { $allSystemsById, $direction, $textMeasureVersion, $userSettings, getUserChartTime } from "@/lib/stores"
|
||||||
import { cn, formatShortDate, isVisuallyLonger, matchesFilterGroups, parseFilterGroups, parseSemVer } from "@/lib/utils"
|
import { cn, formatShortDate, isVisuallyLonger, matchesFilterGroups, parseFilterGroups, parseSemVer } from "@/lib/utils"
|
||||||
import type { ChartOptions, MonitorCertInfo, NetworkMonitorRecord } from "@/types"
|
import type { ChartOptions, MonitorCertInfo, NetworkMonitorRecord } from "@/types"
|
||||||
import { AddMonitorDialog, EditMonitorDialog, SystemMultiSelect } from "./monitor-dialog"
|
import { AddMonitorDialog, EditMonitorDialog, MonitorMultiSelect, SystemMultiSelect } from "./monitor-dialog"
|
||||||
import {
|
import {
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
ArrowLeftRightIcon,
|
ArrowLeftRightIcon,
|
||||||
@@ -67,7 +67,8 @@ import {
|
|||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||||
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||||
import { LossChart, AvgMinMaxResponseChart, ResponseChart } from "@/components/routes/system/charts/monitors-charts"
|
import { LossChart, AvgMinMaxResponseChart, ResponseChart } from "@/components/routes/system/charts/monitors-charts"
|
||||||
import { useMatchingMonitors, useNetworkMonitorStats } from "@/lib/use-network-monitors"
|
import { getMonitorCompareState } from "@/lib/monitor-compare"
|
||||||
|
import { useCompareMonitors, useNetworkMonitorStats } from "@/lib/use-network-monitors"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { atom } from "nanostores"
|
import { atom } from "nanostores"
|
||||||
import { Separator } from "../ui/separator"
|
import { Separator } from "../ui/separator"
|
||||||
@@ -470,6 +471,7 @@ export default function NetworkMonitorsTableNew({
|
|||||||
visibleColumnsKey={visibleColumnsKey}
|
visibleColumnsKey={visibleColumnsKey}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
includesAllSystems={!systemId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -483,6 +485,7 @@ const NetworkMonitorsTable = memo(function NetworkMonitorTable({
|
|||||||
visibleColumnsKey,
|
visibleColumnsKey,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
includesAllSystems,
|
||||||
}: {
|
}: {
|
||||||
table: TableType<NetworkMonitorRecord>
|
table: TableType<NetworkMonitorRecord>
|
||||||
rows: Row<NetworkMonitorRecord>[]
|
rows: Row<NetworkMonitorRecord>[]
|
||||||
@@ -490,6 +493,8 @@ const NetworkMonitorsTable = memo(function NetworkMonitorTable({
|
|||||||
visibleColumnsKey: string
|
visibleColumnsKey: string
|
||||||
rowSelection: RowSelectionState
|
rowSelection: RowSelectionState
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
/** The table lists every system's monitors, so the sheet can compare without fetching. */
|
||||||
|
includesAllSystems: boolean
|
||||||
}) {
|
}) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const [sheetOpen, setSheetOpen] = useState(false)
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
@@ -560,6 +565,8 @@ const NetworkMonitorsTable = memo(function NetworkMonitorTable({
|
|||||||
setSheetOpen(nextOpen)
|
setSheetOpen(nextOpen)
|
||||||
}}
|
}}
|
||||||
monitor={activeMonitor}
|
monitor={activeMonitor}
|
||||||
|
monitors={table.options.data}
|
||||||
|
includesAllSystems={includesAllSystems}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -630,16 +637,29 @@ function NetworkMonitorSheet({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
monitor,
|
monitor,
|
||||||
|
monitors,
|
||||||
|
includesAllSystems,
|
||||||
}: {
|
}: {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
monitor?: NetworkMonitorRecord
|
monitor?: NetworkMonitorRecord
|
||||||
|
monitors: NetworkMonitorRecord[]
|
||||||
|
includesAllSystems: boolean
|
||||||
}) {
|
}) {
|
||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NetworkMonitorSheetContent key={monitor.system} open={open} onOpenChange={onOpenChange} monitor={monitor} />
|
return (
|
||||||
|
<NetworkMonitorSheetContent
|
||||||
|
key={monitor.system}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
monitor={monitor}
|
||||||
|
monitors={monitors}
|
||||||
|
includesAllSystems={includesAllSystems}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const certExpiryTextColors = { ok: "", warning: "text-yellow-600 dark:text-yellow-500", critical: "text-red-500" }
|
const certExpiryTextColors = { ok: "", warning: "text-yellow-600 dark:text-yellow-500", critical: "text-red-500" }
|
||||||
@@ -676,10 +696,16 @@ function NetworkMonitorSheetContent({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
monitor,
|
monitor,
|
||||||
|
monitors,
|
||||||
|
includesAllSystems,
|
||||||
}: {
|
}: {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
monitor: NetworkMonitorRecord
|
monitor: NetworkMonitorRecord
|
||||||
|
/** Table monitors; used to find other targets on the same system to compare against. */
|
||||||
|
monitors: NetworkMonitorRecord[]
|
||||||
|
/** Whether `monitors` covers every system, so other systems' monitors needn't be fetched. */
|
||||||
|
includesAllSystems: boolean
|
||||||
}) {
|
}) {
|
||||||
// Keep monitor exploration independent of the system charts' time range.
|
// Keep monitor exploration independent of the system charts' time range.
|
||||||
const [chartTimeStore] = useState(() => {
|
const [chartTimeStore] = useState(() => {
|
||||||
@@ -691,8 +717,7 @@ function NetworkMonitorSheetContent({
|
|||||||
const systems = useStore($allSystemsById)
|
const systems = useStore($allSystemsById)
|
||||||
const system = systems[monitor.system]
|
const system = systems[monitor.system]
|
||||||
|
|
||||||
// Same target probed from other systems, for side-by-side comparison (#2385).
|
const [compareTargetIds, setCompareTargetIds] = useState<Set<string>>(() => new Set())
|
||||||
const matchingMonitors = useMatchingMonitors(monitor, open)
|
|
||||||
const [compareSystemIds, setCompareSystemIds] = useState<Set<string>>(() => new Set())
|
const [compareSystemIds, setCompareSystemIds] = useState<Set<string>>(() => new Set())
|
||||||
// Scoped to this sheet so a filter doesn't carry over to other monitors' sheets.
|
// Scoped to this sheet so a filter doesn't carry over to other monitors' sheets.
|
||||||
const [compareFilterStore, setCompareFilterStore] = useState(() => atom(""))
|
const [compareFilterStore, setCompareFilterStore] = useState(() => atom(""))
|
||||||
@@ -701,16 +726,25 @@ function NetworkMonitorSheetContent({
|
|||||||
if (compareMonitorId !== monitor.id) {
|
if (compareMonitorId !== monitor.id) {
|
||||||
setCompareMonitorId(monitor.id)
|
setCompareMonitorId(monitor.id)
|
||||||
setCompareSystemIds(new Set())
|
setCompareSystemIds(new Set())
|
||||||
|
setCompareTargetIds(new Set())
|
||||||
setCompareFilterStore(atom(""))
|
setCompareFilterStore(atom(""))
|
||||||
}
|
}
|
||||||
const matchingSystemIds = useMemo(() => matchingMonitors.map((m) => m.system), [matchingMonitors])
|
// Other systems' monitors come from the table when it lists every system, otherwise from one fetch.
|
||||||
// The opened system is always charted; the picker only adds other systems to compare against.
|
const fetchedMonitors = useCompareMonitors(monitor.system, monitor.protocol, open && !includesAllSystems)
|
||||||
const compareMonitors = useMemo(
|
const compare = useMemo(
|
||||||
() => [monitor, ...matchingMonitors.filter((m) => compareSystemIds.has(m.system))],
|
() =>
|
||||||
[monitor, matchingMonitors, compareSystemIds]
|
getMonitorCompareState({
|
||||||
|
monitor,
|
||||||
|
localMonitors: monitors,
|
||||||
|
otherMonitors: includesAllSystems ? monitors : fetchedMonitors,
|
||||||
|
selectedSystemIds: compareSystemIds,
|
||||||
|
selectedTargetIds: compareTargetIds,
|
||||||
|
getSystemName: (id) => systems[id]?.name ?? id,
|
||||||
|
}),
|
||||||
|
[monitor, monitors, includesAllSystems, fetchedMonitors, compareSystemIds, compareTargetIds, systems]
|
||||||
)
|
)
|
||||||
|
const { compareMonitors } = compare
|
||||||
const comparing = compareMonitors.length > 1
|
const comparing = compareMonitors.length > 1
|
||||||
const getSystemName = useCallback((m: NetworkMonitorRecord) => systems[m.system]?.name ?? m.system, [systems])
|
|
||||||
|
|
||||||
const monitorStats = useNetworkMonitorStats({
|
const monitorStats = useNetworkMonitorStats({
|
||||||
systemId: monitor.system,
|
systemId: monitor.system,
|
||||||
@@ -766,21 +800,31 @@ function NetworkMonitorSheetContent({
|
|||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<ChartTimeSelect
|
<ChartTimeSelect
|
||||||
className="bg-card flex-1 basis-48"
|
className="bg-card flex-1 min-w-0 basis-full sm:basis-0"
|
||||||
agentVersion={chartData.agentVersion}
|
agentVersion={chartData.agentVersion}
|
||||||
chartTimeStore={chartTimeStore}
|
chartTimeStore={chartTimeStore}
|
||||||
allowRealtime={false}
|
allowRealtime={false}
|
||||||
/>
|
/>
|
||||||
{matchingMonitors.length > 0 && (
|
<MonitorMultiSelect
|
||||||
|
id="monitor-compare-targets"
|
||||||
|
className="flex-1 min-w-0 basis-full sm:basis-0 bg-card"
|
||||||
|
monitors={compare.targetOptions}
|
||||||
|
selectedMonitorIds={compare.selectedTargetIds}
|
||||||
|
onChange={setCompareTargetIds}
|
||||||
|
disabled={compare.targetOptions.length === 0}
|
||||||
|
canSelectMore={compare.canAddTarget}
|
||||||
|
placeholder={t`Compare with other targets`}
|
||||||
|
/>
|
||||||
<SystemMultiSelect
|
<SystemMultiSelect
|
||||||
id="monitor-compare-systems"
|
id="monitor-compare-systems"
|
||||||
className="w-full sm:w-1/3 shrink-0 bg-card"
|
className="flex-1 min-w-0 basis-full sm:basis-0 bg-card"
|
||||||
systemIds={matchingSystemIds}
|
systemIds={compare.systemOptions}
|
||||||
selectedSystemIds={compareSystemIds}
|
selectedSystemIds={compare.selectedSystemIds}
|
||||||
onChange={setCompareSystemIds}
|
onChange={setCompareSystemIds}
|
||||||
|
disabled={compare.systemOptions.length === 0}
|
||||||
|
canSelectMore={compare.canAddSystem}
|
||||||
placeholder={t`Compare with other systems`}
|
placeholder={t`Compare with other systems`}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{comparing ? (
|
{comparing ? (
|
||||||
<>
|
<>
|
||||||
@@ -790,7 +834,7 @@ function NetworkMonitorSheetContent({
|
|||||||
monitors={compareMonitors}
|
monitors={compareMonitors}
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
empty={!hasMonitorStats}
|
empty={!hasMonitorStats}
|
||||||
getLabel={getSystemName}
|
getLabel={compare.getLabel}
|
||||||
filterStore={compareFilterStore}
|
filterStore={compareFilterStore}
|
||||||
/>
|
/>
|
||||||
<LossChart
|
<LossChart
|
||||||
@@ -799,7 +843,7 @@ function NetworkMonitorSheetContent({
|
|||||||
monitors={compareMonitors}
|
monitors={compareMonitors}
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
empty={!hasMonitorStats}
|
empty={!hasMonitorStats}
|
||||||
getLabel={getSystemName}
|
getLabel={compare.getLabel}
|
||||||
filterStore={compareFilterStore}
|
filterStore={compareFilterStore}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import { memo, type ReactNode, useEffect, useMemo, useRef, useState } from "reac
|
|||||||
import { getStatusColor, systemdTableCols } from "@/components/systemd-table/systemd-table-columns"
|
import { getStatusColor, systemdTableCols } from "@/components/systemd-table/systemd-table-columns"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { LogsDisplay, LogsFullscreenDialog } from "@/components/logs-display"
|
import { LogsDisplay, LogsFullscreenDialog, LogsIconButton, LogsTimestampToggle } from "@/components/logs-display"
|
||||||
|
import { getLogTimestampDecorations } from "@/lib/logs"
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
@@ -41,7 +41,13 @@ async function getSystemdLogsHtml(systemId: string, serviceName: string): Promis
|
|||||||
query: { system: systemId, service: serviceName },
|
query: { system: systemId, service: serviceName },
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
return logs ? highlighter.codeToHtml(logs, { lang: "log", theme: syntaxTheme }) : ""
|
return logs
|
||||||
|
? highlighter.codeToHtml(logs, {
|
||||||
|
lang: "log",
|
||||||
|
theme: syntaxTheme,
|
||||||
|
decorations: getLogTimestampDecorations(logs),
|
||||||
|
})
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemdTable({ systemId }: { systemId?: string }) {
|
export default function SystemdTable({ systemId }: { systemId?: string }) {
|
||||||
@@ -526,28 +532,15 @@ function SystemdSheet({
|
|||||||
<h3 className="text-sm font-medium">
|
<h3 className="text-sm font-medium">
|
||||||
<Trans>Logs</Trans>
|
<Trans>Logs</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
<Button
|
<LogsTimestampToggle className="ms-auto" />
|
||||||
variant="ghost"
|
<LogsIconButton label={t`Refresh`} onClick={loadLogs} disabled={isLoadingLogs}>
|
||||||
size="sm"
|
|
||||||
onClick={loadLogs}
|
|
||||||
className="h-8 w-8 p-0 ms-auto"
|
|
||||||
disabled={isLoadingLogs}
|
|
||||||
aria-label={t`Refresh`}
|
|
||||||
>
|
|
||||||
<RefreshCwIcon
|
<RefreshCwIcon
|
||||||
className={cn("size-4 transition-transform duration-300", isLoadingLogs && "animate-spin")}
|
className={cn("size-4 transition-transform duration-300", isLoadingLogs && "animate-spin")}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</LogsIconButton>
|
||||||
<Button
|
<LogsIconButton label={t`Fullscreen`} onClick={() => setLogsFullscreenOpen(true)} disabled={!logs}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setLogsFullscreenOpen(true)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
aria-label={t`Logs`}
|
|
||||||
disabled={!logs}
|
|
||||||
>
|
|
||||||
<MaximizeIcon className="size-4" />
|
<MaximizeIcon className="size-4" />
|
||||||
</Button>
|
</LogsIconButton>
|
||||||
</div>
|
</div>
|
||||||
{logs ? (
|
{logs ? (
|
||||||
<LogsDisplay logsDisplay={logs} containerRef={logsContainerRef} />
|
<LogsDisplay logsDisplay={logs} containerRef={logsContainerRef} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const alertVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground [&>svg]:text-foreground",
|
default: "bg-background text-foreground [&>svg]:text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive/5 border-destructive/50 text-destructive dark:bg-destructive/6 dark:text-red-500 dark:border-destructive/70 [&>svg]:text-destructive",
|
"bg-destructive/5 border-destructive/50 text-destructive dark:bg-destructive/6 dark:text-red-400 dark:border-destructive/50 [&>svg]:text-destructive dark:[&>svg]:text-red-400",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export const alertInfo: Record<string, AlertInfo> = {
|
|||||||
icon: EthernetIcon,
|
icon: EthernetIcon,
|
||||||
desc: () => t`Triggers when combined up/down exceeds a threshold`,
|
desc: () => t`Triggers when combined up/down exceeds a threshold`,
|
||||||
max: 250,
|
max: 250,
|
||||||
|
// 1024 to match the hub, which converts bandwidth with 1024 * 1024
|
||||||
|
units: [
|
||||||
|
{ unit: " MB/s", factor: 1, min: 1, max: 250, step: 1, inputStep: 0.1 },
|
||||||
|
{ unit: " GB/s", factor: 1024, min: 0.1, max: 5, step: 0.1 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
NetworkMonitorLoss: {
|
NetworkMonitorLoss: {
|
||||||
name: () => t`Network Monitor Loss`,
|
name: () => t`Network Monitor Loss`,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { DecorationItem } from "shiki/core"
|
||||||
|
|
||||||
|
// Docker uses RFC3339Nano; journalctl short-iso uses an offset such as +0200.
|
||||||
|
// Only mark the first timestamp, leaving dates in the log message untouched.
|
||||||
|
export function getLogTimestampDecorations(logs: string): DecorationItem[] {
|
||||||
|
const timestamps = logs.matchAll(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2}) /gm)
|
||||||
|
return Array.from(timestamps, (match) => ({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + match[0].length,
|
||||||
|
properties: { class: "log-timestamp" },
|
||||||
|
alwaysWrap: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import type { NetworkMonitorRecord } from "@/types"
|
||||||
|
import {
|
||||||
|
getMonitorCompareState,
|
||||||
|
getMonitorIdentityKey,
|
||||||
|
getMonitorTarget,
|
||||||
|
MAX_COMPARE_MONITORS,
|
||||||
|
} from "./monitor-compare"
|
||||||
|
|
||||||
|
function mon(id: string, system: string, target: string, extra: Partial<NetworkMonitorRecord> = {}) {
|
||||||
|
return { id, system, target, protocol: "icmp", port: 0, server: "", interval: 30, ...extra } as NetworkMonitorRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemNames: Record<string, string> = { a: "Alpha", b: "Bravo", c: "Charlie" }
|
||||||
|
|
||||||
|
function state(
|
||||||
|
monitor: NetworkMonitorRecord,
|
||||||
|
monitors: NetworkMonitorRecord[],
|
||||||
|
selectedSystemIds: string[] = [],
|
||||||
|
selectedTargetIds: string[] = []
|
||||||
|
) {
|
||||||
|
return getMonitorCompareState({
|
||||||
|
monitor,
|
||||||
|
localMonitors: monitors,
|
||||||
|
otherMonitors: monitors,
|
||||||
|
selectedSystemIds: new Set(selectedSystemIds),
|
||||||
|
selectedTargetIds: new Set(selectedTargetIds),
|
||||||
|
getSystemName: (id) => systemNames[id] ?? id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = (monitors: NetworkMonitorRecord[]) => monitors.map((m) => m.id)
|
||||||
|
|
||||||
|
// a probes one and two; b probes one and two; c probes only one
|
||||||
|
const a1 = mon("a1", "a", "one.example")
|
||||||
|
const a2 = mon("a2", "a", "two.example")
|
||||||
|
const b1 = mon("b1", "b", "one.example")
|
||||||
|
const b2 = mon("b2", "b", "two.example")
|
||||||
|
const c1 = mon("c1", "c", "one.example")
|
||||||
|
const all = [a1, a2, b1, b2, c1]
|
||||||
|
|
||||||
|
test("formats tcp targets with their port", () => {
|
||||||
|
expect(getMonitorTarget({ target: "example.com", protocol: "icmp", port: 0 })).toBe("example.com")
|
||||||
|
expect(getMonitorTarget({ target: "example.com", protocol: "tcp", port: 443 })).toBe("example.com:443")
|
||||||
|
expect(getMonitorTarget({ target: "::1", protocol: "tcp", port: 22 })).toBe("[::1]:22")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("identity ignores the system but not protocol, port or server", () => {
|
||||||
|
expect(getMonitorIdentityKey(a1)).toBe(getMonitorIdentityKey(b1))
|
||||||
|
expect(getMonitorIdentityKey(a1)).not.toBe(getMonitorIdentityKey({ ...a1, protocol: "http" }))
|
||||||
|
expect(getMonitorIdentityKey(a1)).not.toBe(getMonitorIdentityKey({ ...a1, port: 80 }))
|
||||||
|
expect(getMonitorIdentityKey(a1)).not.toBe(getMonitorIdentityKey({ ...a1, server: "1.1.1.1" }))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("with nothing selected only the opened monitor is charted", () => {
|
||||||
|
const s = state(a1, all)
|
||||||
|
expect(s.systemOptions).toEqual(["b", "c"])
|
||||||
|
expect(ids(s.targetOptions)).toEqual(["a2"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("only offers targets and systems with the same protocol", () => {
|
||||||
|
const aHttp = mon("aHttp", "a", "https://two.example", { protocol: "http" })
|
||||||
|
const bTcp = mon("bTcp", "b", "one.example", { protocol: "tcp", port: 443 })
|
||||||
|
const s = state(a1, [a1, a2, aHttp, bTcp, c1])
|
||||||
|
expect(ids(s.targetOptions)).toEqual(["a2"])
|
||||||
|
expect(s.systemOptions).toEqual(["c"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("comparing targets charts them on the opened system, labelled by target", () => {
|
||||||
|
const s = state(a1, all, [], ["a2"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "a2"])
|
||||||
|
expect(s.getLabel(a1)).toBe("one.example")
|
||||||
|
expect(s.getLabel(a2)).toBe("two.example")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("comparing systems charts the opened target on them, labelled by system", () => {
|
||||||
|
const s = state(a1, all, ["b", "c"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "b1", "c1"])
|
||||||
|
expect(s.getLabel(a1)).toBe("Alpha")
|
||||||
|
expect(s.getLabel(c1)).toBe("Charlie")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("each compared target is charted on every selected system", () => {
|
||||||
|
const s = state(a1, all, ["b"], ["a2"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "a2", "b1", "b2"])
|
||||||
|
expect(s.getLabel(b2)).toBe("Bravo · two.example")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("selecting a system limits targets to ones it also probes", () => {
|
||||||
|
const s = state(a1, all, ["c"])
|
||||||
|
expect(ids(s.targetOptions)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("selecting a target limits systems to ones that also probe it", () => {
|
||||||
|
const s = state(a1, all, [], ["a2"])
|
||||||
|
expect(s.systemOptions).toEqual(["b"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the pickers can't produce conflicting selections, but targets win if they conflict", () => {
|
||||||
|
const s = state(a1, all, ["c"], ["a2"])
|
||||||
|
expect(s.selectedSystemIds.size).toBe(0)
|
||||||
|
expect([...s.selectedTargetIds]).toEqual(["a2"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "a2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("selected systems that are no longer options are ignored", () => {
|
||||||
|
const s = state(a1, all, ["gone"])
|
||||||
|
expect(s.selectedSystemIds.size).toBe(0)
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("DNS lookups of the same name against different servers get the server in their label", () => {
|
||||||
|
const d1 = mon("d1", "a", "example.com", { protocol: "dns", server: "1.1.1.1" })
|
||||||
|
const d2 = mon("d2", "a", "example.com", { protocol: "dns", server: "8.8.8.8" })
|
||||||
|
const s = state(d1, [d1, d2], [], ["d2"])
|
||||||
|
expect(s.getLabel(d1)).toBe("example.com (1.1.1.1)")
|
||||||
|
expect(s.getLabel(d2)).toBe("example.com (8.8.8.8)")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses separate local and fetched monitor lists on single-system pages", () => {
|
||||||
|
const s = getMonitorCompareState({
|
||||||
|
monitor: a1,
|
||||||
|
localMonitors: [a1, a2],
|
||||||
|
otherMonitors: [b1, b2],
|
||||||
|
selectedSystemIds: new Set(["b"]),
|
||||||
|
selectedTargetIds: new Set(["a2"]),
|
||||||
|
getSystemName: (id) => systemNames[id],
|
||||||
|
})
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "a2", "b1", "b2"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("duplicate labels without a server are left alone", () => {
|
||||||
|
const b1Dupe = mon("b1Dupe", "b", "one.example")
|
||||||
|
const s = state(a1, [...all, b1Dupe], ["b"])
|
||||||
|
expect(ids(s.compareMonitors)).toEqual(["a1", "b1", "b1Dupe"])
|
||||||
|
expect(s.getLabel(b1)).toBe("Bravo")
|
||||||
|
expect(s.getLabel(b1Dupe)).toBe("Bravo")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Every system probes every target, so selections multiply into targets × systems lines.
|
||||||
|
function grid(systemCount: number, targetCount: number) {
|
||||||
|
const monitors: NetworkMonitorRecord[] = []
|
||||||
|
for (let s = 0; s < systemCount; s++) {
|
||||||
|
for (let t = 0; t < targetCount; t++) monitors.push(mon(`s${s}t${t}`, `s${s}`, `t${t}.example`))
|
||||||
|
}
|
||||||
|
return monitors
|
||||||
|
}
|
||||||
|
|
||||||
|
test("selecting everything is trimmed to the line limit, keeping targets and earlier picks", () => {
|
||||||
|
const monitors = grid(30, 5)
|
||||||
|
const allSystems = Array.from({ length: 29 }, (_, i) => `s${i + 1}`)
|
||||||
|
const allTargets = ["s0t1", "s0t2", "s0t3", "s0t4"]
|
||||||
|
const s = state(monitors[0], monitors, allSystems, allTargets)
|
||||||
|
expect([...s.selectedTargetIds]).toEqual(allTargets)
|
||||||
|
// 5 targets fit on 4 systems (the opened one plus 3) within the limit
|
||||||
|
expect([...s.selectedSystemIds]).toEqual(["s1", "s2", "s3"])
|
||||||
|
expect(s.compareMonitors.length).toBe(20)
|
||||||
|
expect(s.canAddSystem).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("more can be added only while the next pick fits the line limit", () => {
|
||||||
|
const monitors = grid(12, 3)
|
||||||
|
const s = state(monitors[0], monitors, ["s1", "s2", "s3", "s4", "s5", "s6"])
|
||||||
|
// 1 target × 7 systems; another target makes 14, another system makes 8
|
||||||
|
expect(s.canAddTarget).toBe(true)
|
||||||
|
expect(s.canAddSystem).toBe(true)
|
||||||
|
const full = state(monitors[0], monitors, ["s1", "s2", "s3", "s4", "s5", "s6", "s7"], ["s0t1", "s0t2"])
|
||||||
|
// 3 targets × 8 systems = 24
|
||||||
|
expect(full.compareMonitors.length).toBe(MAX_COMPARE_MONITORS)
|
||||||
|
expect(full.canAddTarget).toBe(false)
|
||||||
|
expect(full.canAddSystem).toBe(false)
|
||||||
|
})
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import type { NetworkMonitorRecord } from "@/types"
|
||||||
|
|
||||||
|
// Kept free of UI and store imports so it can be unit tested with bun.
|
||||||
|
|
||||||
|
/** Most lines a comparison charts, to keep it readable and the stats request's filter short. */
|
||||||
|
export const MAX_COMPARE_MONITORS = 24
|
||||||
|
|
||||||
|
type MonitorTarget = Pick<NetworkMonitorRecord, "target" | "protocol" | "port">
|
||||||
|
|
||||||
|
export function getMonitorTarget(monitor: MonitorTarget) {
|
||||||
|
if (monitor.protocol !== "tcp") return monitor.target
|
||||||
|
const host = monitor.target.includes(":") && !monitor.target.startsWith("[") ? `[${monitor.target}]` : monitor.target
|
||||||
|
return `${host}:${monitor.port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Identifies what a monitor probes, regardless of which system probes it. */
|
||||||
|
export function getMonitorIdentityKey({
|
||||||
|
protocol,
|
||||||
|
target,
|
||||||
|
port,
|
||||||
|
server,
|
||||||
|
}: Pick<NetworkMonitorRecord, "protocol" | "target" | "port" | "server">) {
|
||||||
|
return JSON.stringify([protocol, target, port, server])
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonitorCompareInput {
|
||||||
|
/** The monitor whose sheet is open; always charted. */
|
||||||
|
monitor: NetworkMonitorRecord
|
||||||
|
/** Monitors that may include other targets on the opened monitor's system. */
|
||||||
|
localMonitors: NetworkMonitorRecord[]
|
||||||
|
/** Monitors that may include other systems' monitors. */
|
||||||
|
otherMonitors: NetworkMonitorRecord[]
|
||||||
|
selectedSystemIds: Set<string>
|
||||||
|
selectedTargetIds: Set<string>
|
||||||
|
getSystemName: (systemId: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works out what the monitor sheet can compare and what it charts. Comparisons are limited to the opened
|
||||||
|
* monitor's protocol, since response time and loss mean different things per protocol.
|
||||||
|
*/
|
||||||
|
export function getMonitorCompareState({
|
||||||
|
monitor,
|
||||||
|
localMonitors,
|
||||||
|
otherMonitors,
|
||||||
|
selectedSystemIds,
|
||||||
|
selectedTargetIds,
|
||||||
|
getSystemName,
|
||||||
|
}: MonitorCompareInput) {
|
||||||
|
const sameProtocol = (m: NetworkMonitorRecord) => m.protocol === monitor.protocol
|
||||||
|
const systemTargets = localMonitors.filter(
|
||||||
|
(m) => m.system === monitor.system && m.id !== monitor.id && sameProtocol(m)
|
||||||
|
)
|
||||||
|
const systemMonitors = otherMonitors.filter((m) => m.system !== monitor.system && sameProtocol(m))
|
||||||
|
|
||||||
|
const keysBySystem = new Map<string, Set<string>>()
|
||||||
|
for (const m of systemMonitors) {
|
||||||
|
const keys = keysBySystem.get(m.system) ?? new Set<string>()
|
||||||
|
keys.add(getMonitorIdentityKey(m))
|
||||||
|
keysBySystem.set(m.system, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each picker only offers what fits the other's selection, so every pick charts a line per system:
|
||||||
|
// systems must probe the opened target and every selected target, and targets must be probed by
|
||||||
|
// every selected system. Selections outside the options (e.g. a monitor deleted while the sheet is
|
||||||
|
// open) are ignored, and ones past MAX_COMPARE_MONITORS lines are dropped, keeping targets over
|
||||||
|
// systems and earlier picks over later ones.
|
||||||
|
const systemTargetsById = new Map(systemTargets.map((m) => [m.id, m]))
|
||||||
|
const pickedTargets = [...selectedTargetIds]
|
||||||
|
.flatMap((id) => systemTargetsById.get(id) ?? [])
|
||||||
|
.slice(0, MAX_COMPARE_MONITORS - 1)
|
||||||
|
const targetMonitors = [monitor, ...pickedTargets]
|
||||||
|
const requiredKeys = targetMonitors.map(getMonitorIdentityKey)
|
||||||
|
const systemOptions = [...keysBySystem]
|
||||||
|
.filter(([, keys]) => requiredKeys.every((key) => keys.has(key)))
|
||||||
|
.map(([id]) => id)
|
||||||
|
const systemOptionSet = new Set(systemOptions)
|
||||||
|
const systemIds = [...selectedSystemIds]
|
||||||
|
.filter((id) => systemOptionSet.has(id))
|
||||||
|
.slice(0, Math.floor(MAX_COMPARE_MONITORS / targetMonitors.length) - 1)
|
||||||
|
|
||||||
|
const targetOptions = systemTargets.filter((m) => {
|
||||||
|
const key = getMonitorIdentityKey(m)
|
||||||
|
return systemIds.every((id) => keysBySystem.get(id)?.has(key))
|
||||||
|
})
|
||||||
|
const targetIds = new Set(pickedTargets.map((m) => m.id))
|
||||||
|
|
||||||
|
// Every charted target is also charted for each selected system.
|
||||||
|
const targetKeys = new Set(targetMonitors.map(getMonitorIdentityKey))
|
||||||
|
const selectedSystems = new Set(systemIds)
|
||||||
|
const compareMonitors = [
|
||||||
|
...targetMonitors,
|
||||||
|
...systemMonitors.filter((m) => selectedSystems.has(m.system) && targetKeys.has(getMonitorIdentityKey(m))),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Label series by whatever differs between them: system, target, or both.
|
||||||
|
const multiSystem = systemIds.length > 0
|
||||||
|
const multiTarget = targetIds.size > 0
|
||||||
|
const labels = new Map<string, string>()
|
||||||
|
const counts = new Map<string, number>()
|
||||||
|
for (const m of compareMonitors) {
|
||||||
|
const systemName = getSystemName(m.system)
|
||||||
|
const target = getMonitorTarget(m)
|
||||||
|
const label = multiSystem && multiTarget ? `${systemName} · ${target}` : multiTarget ? target : systemName
|
||||||
|
labels.set(m.id, label)
|
||||||
|
counts.set(label, (counts.get(label) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
// DNS lookups of the same name against different servers would otherwise share a label.
|
||||||
|
for (const m of compareMonitors) {
|
||||||
|
const label = labels.get(m.id) as string
|
||||||
|
if ((counts.get(label) ?? 0) > 1 && m.server) labels.set(m.id, `${label} (${m.server})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Systems that can be selected to compare against. */
|
||||||
|
systemOptions,
|
||||||
|
selectedSystemIds: new Set(systemIds),
|
||||||
|
/** Other targets on the opened monitor's system that can be selected. */
|
||||||
|
targetOptions,
|
||||||
|
selectedTargetIds: targetIds,
|
||||||
|
/** Whether one more target or system still fits within MAX_COMPARE_MONITORS lines. */
|
||||||
|
canAddTarget: (targetMonitors.length + 1) * (systemIds.length + 1) <= MAX_COMPARE_MONITORS,
|
||||||
|
canAddSystem: targetMonitors.length * (systemIds.length + 2) <= MAX_COMPARE_MONITORS,
|
||||||
|
/** Monitors to chart, starting with the opened one. */
|
||||||
|
compareMonitors,
|
||||||
|
getLabel: (m: NetworkMonitorRecord) => labels.get(m.id) ?? getMonitorTarget(m),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,11 +98,7 @@ export function mergeMonitorStats(rawRecords: RawMonitorStatsRecord[], bucketMs
|
|||||||
.map(([created, stats]) => ({ created, stats }))
|
.map(([created, stats]) => ({ created, stats }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMonitorTarget(monitor: Pick<NetworkMonitorRecord, "target" | "protocol" | "port">) {
|
export { getMonitorTarget } from "./monitor-compare"
|
||||||
if (monitor.protocol !== "tcp") return monitor.target
|
|
||||||
const host = monitor.target.includes(":") && !monitor.target.startsWith("[") ? `[${monitor.target}]` : monitor.target
|
|
||||||
return `${host}:${monitor.port}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whole days until the certificate expires; negative once expired. */
|
/** Whole days until the certificate expires; negative once expired. */
|
||||||
export function getCertDaysLeft(cert: Pick<MonitorCertInfo, "expires">, now = Date.now()) {
|
export function getCertDaysLeft(cert: Pick<MonitorCertInfo, "expires">, now = Date.now()) {
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ export const $chartTime = atom<ChartTimes>(defaultChartTime)
|
|||||||
/** Whether to display average or max chart values */
|
/** Whether to display average or max chart values */
|
||||||
export const $maxValues = atom(false)
|
export const $maxValues = atom(false)
|
||||||
|
|
||||||
|
const logTimestampsKey = "besz-log-ts"
|
||||||
|
|
||||||
|
/** Whether to show timestamps in Docker and systemd logs */
|
||||||
|
export const $showLogTimestamps = atom<boolean>(JSON.parse(localStorage.getItem(logTimestampsKey) ?? "true"))
|
||||||
|
|
||||||
|
export function toggleLogTimestamps() {
|
||||||
|
const next = !$showLogTimestamps.get()
|
||||||
|
$showLogTimestamps.set(next)
|
||||||
|
localStorage.setItem(logTimestampsKey, JSON.stringify(next))
|
||||||
|
}
|
||||||
|
|
||||||
// export const UserSettingsSchema = v.object({
|
// export const UserSettingsSchema = v.object({
|
||||||
// chartTime: v.picklist(["1h", "12h", "24h", "1w", "30d"]),
|
// chartTime: v.picklist(["1h", "12h", "24h", "1w", "30d"]),
|
||||||
// emails: v.optional(v.array(v.pipe(v.string(), v.email())), [pb?.authStore?.record?.email ?? ""]),
|
// emails: v.optional(v.array(v.pipe(v.string(), v.email())), [pb?.authStore?.record?.email ?? ""]),
|
||||||
|
|||||||
@@ -323,38 +323,38 @@ export function useNetworkMonitorStats(props: UseNetworkMonitorStatsProps) {
|
|||||||
}, [monitorStats, cacheKey, interval, chartTime])
|
}, [monitorStats, cacheKey, interval, chartTime])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Only what comparison charts and labels need. */
|
||||||
|
const COMPARE_MONITOR_FIELDS = "id,system,target,protocol,port,server,interval,resAvg1h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitors on other systems that probe the same target (same protocol, target, port, and DNS server).
|
* Monitors of one protocol on all systems except the given one, to compare against (#2385).
|
||||||
* Fetched once per open so it also works in single-system tables, which only hold one system's monitors.
|
* Fetched per open so it also works in single-system tables, which only hold one system's monitors.
|
||||||
*/
|
*/
|
||||||
export function useMatchingMonitors(monitor: NetworkMonitorRecord, enabled = true) {
|
export function useCompareMonitors(system: string, protocol: string, enabled = true) {
|
||||||
const [matches, setMatches] = useState<NetworkMonitorRecord[]>([])
|
const key = `${system}:${protocol}`
|
||||||
const { id, system, protocol, target, port, server } = monitor
|
const [result, setResult] = useState<{ key: string; monitors: NetworkMonitorRecord[] }>({ key, monitors: [] })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMatches([])
|
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
pb.collection<NetworkMonitorRecord>("network_monitors")
|
pb.collection<NetworkMonitorRecord>("network_monitors")
|
||||||
.getFullList({
|
.getFullList({
|
||||||
fields: NETWORK_MONITOR_FIELDS,
|
fields: COMPARE_MONITOR_FIELDS,
|
||||||
filter: pb.filter(
|
filter: pb.filter("system!={:system} && protocol={:protocol}", { system, protocol }),
|
||||||
"id!={:id} && system!={:system} && protocol={:protocol} && target={:target} && port={:port} && server={:server}",
|
|
||||||
{ id, system, protocol, target, port, server }
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
.then((records) => {
|
.then((monitors) => {
|
||||||
if (!cancelled) setMatches(records)
|
if (!cancelled) setResult({ key: `${system}:${protocol}`, monitors })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (!cancelled) console.error("Failed to fetch matching monitors:", error)
|
if (!cancelled) console.error("Failed to fetch compare monitors:", error)
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [id, system, protocol, target, port, server, enabled])
|
}, [system, protocol, enabled])
|
||||||
|
|
||||||
return matches
|
// Keep showing the last result while reopening refreshes it, but never another monitor's.
|
||||||
|
return result.key === key ? result.monitors : []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMonitors(system?: string) {
|
async function fetchMonitors(system?: string) {
|
||||||
|
|||||||
@@ -229,14 +229,14 @@ function getStorageValue(key: string, defaultValue: unknown, storageInterface: S
|
|||||||
/** Hook to sync value in local or session storage */
|
/** Hook to sync value in local or session storage */
|
||||||
export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
|
export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
|
||||||
key = `besz-${key}`
|
key = `besz-${key}`
|
||||||
const [value, setValue] = useState(() => {
|
const [value, setValue] = useState<T>(() => {
|
||||||
return getStorageValue(key, defaultValue, storageInterface)
|
return getStorageValue(key, defaultValue, storageInterface)
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
storageInterface?.setItem(key, JSON.stringify(value))
|
storageInterface?.setItem(key, JSON.stringify(value))
|
||||||
}, [key, value])
|
}, [key, value])
|
||||||
|
|
||||||
return [value, setValue]
|
return [value, setValue] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format temperature to user's preferred unit */
|
/** Format temperature to user's preferred unit */
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ar\n"
|
"Language: ar\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Arabic\n"
|
"Language-Team: Arabic\n"
|
||||||
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "نعم"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: bg\n"
|
"Language: bg\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:28\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Bulgarian\n"
|
"Language-Team: Bulgarian\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Да"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Настройките за потребителя ти са обновени."
|
msgstr "Настройките за потребителя ти са обновени."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: cs\n"
|
"Language: cs\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:28\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Czech\n"
|
"Language-Team: Czech\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ano"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: da\n"
|
"Language: da\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:28\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Danish\n"
|
"Language-Team: Danish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ja"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dine brugerindstillinger er opdateret."
|
msgstr "Dine brugerindstillinger er opdateret."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ja"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: el\n"
|
"Language: el\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Greek\n"
|
"Language-Team: Greek\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ναι"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Οι ρυθμίσεις χρήστη σας ενημερώθηκαν."
|
msgstr "Οι ρυθμίσεις χρήστη σας ενημερώθηκαν."
|
||||||
|
|
||||||
|
|||||||
@@ -2354,3 +2354,11 @@ msgstr "Yes"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Your user settings have been updated."
|
msgstr "Your user settings have been updated."
|
||||||
|
|
||||||
|
#: src/components/logs-display.tsx
|
||||||
|
msgid "Show timestamps"
|
||||||
|
msgstr "Show timestamps"
|
||||||
|
|
||||||
|
#: src/components/logs-display.tsx
|
||||||
|
msgid "Hide timestamps"
|
||||||
|
msgstr "Hide timestamps"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:28\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Spanish\n"
|
"Language-Team: Spanish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Sí"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Tu configuración de usuario ha sido actualizada."
|
msgstr "Tu configuración de usuario ha sido actualizada."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: fa\n"
|
"Language: fa\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Persian\n"
|
"Language-Team: Persian\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "بله"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-25 14:03\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: French\n"
|
"Language-Team: French\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
@@ -54,7 +54,7 @@ msgstr "{count, plural, one {{countString} minute} other {{countString} minutes}
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
msgstr ""
|
msgstr "{daysLeft, plural, one {# jour} other {# jours}}"
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
@@ -62,7 +62,7 @@ msgstr "E/S {diskName}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
msgstr ""
|
msgstr "{numFailed, plural, one {# service en échec} other {# services en échec}}"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
@@ -70,7 +70,7 @@ msgstr "{threads, plural, one {# thread} other {# threads}}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{totalCount, plural, one {# service} other {# services}}"
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
msgstr ""
|
msgstr "{totalCount, plural, one {# service} other {# services}}"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
@@ -137,7 +137,7 @@ msgstr "État actif"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr "Ajouter"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -406,15 +406,15 @@ msgstr "Celsius (°C)"
|
|||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Certificate"
|
msgid "Certificate"
|
||||||
msgstr ""
|
msgstr "Certificat"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expired {expires}"
|
msgid "Certificate expired {expires}"
|
||||||
msgstr ""
|
msgstr "Certificat expiré {expires}"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expires {expires}"
|
msgid "Certificate expires {expires}"
|
||||||
msgstr ""
|
msgstr "Le certificat expire le {expires}"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
@@ -905,7 +905,7 @@ msgstr "Sorti actif"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr "Expiré"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Oui"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: he\n"
|
"Language: he\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Hebrew\n"
|
"Language-Team: Hebrew\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "כן"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "הגדרות המשתמש שלך עודכנו."
|
msgstr "הגדרות המשתמש שלך עודכנו."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: hr\n"
|
"Language: hr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Croatian\n"
|
"Language-Team: Croatian\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Da"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše korisničke postavke su ažurirane."
|
msgstr "Vaše korisničke postavke su ažurirane."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: hu\n"
|
"Language: hu\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Hungarian\n"
|
"Language-Team: Hungarian\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Igen"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "A felhasználói beállítások frissítésre kerültek."
|
msgstr "A felhasználói beállítások frissítésre kerültek."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: id\n"
|
"Language: id\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Indonesian\n"
|
"Language-Team: Indonesian\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ya"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Pengaturan pengguna anda telah diperbarui."
|
msgstr "Pengaturan pengguna anda telah diperbarui."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Italian\n"
|
"Language-Team: Italian\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Sì"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Le impostazioni utente sono state aggiornate."
|
msgstr "Le impostazioni utente sono state aggiornate."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ja\n"
|
"Language: ja\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-10-01 01:10\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Japanese\n"
|
"Language-Team: Japanese\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -54,7 +54,7 @@ msgstr "{count, plural, one {{countString} 分} few {{countString} 分} many {{c
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
msgstr ""
|
msgstr "{daysLeft, plural, one {# 日} other {# 日}}"
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
@@ -62,7 +62,7 @@ msgstr "{diskName} の I/O"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
msgstr ""
|
msgstr "{numFailed, plural, one {# 失敗したサービス} other {# 失敗したサービス}}"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
@@ -70,7 +70,7 @@ msgstr "{threads, plural, one {# スレッド} other {# スレッド}}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{totalCount, plural, one {# service} other {# services}}"
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
msgstr ""
|
msgstr "{totalCount, plural, one {# サービス} other {# サービス}}"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
@@ -137,7 +137,7 @@ msgstr "アクティブ状態"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr "追加"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -189,7 +189,7 @@ msgstr "エージェント"
|
|||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Alert History"
|
msgid "Alert History"
|
||||||
msgstr "アラート履歴"
|
msgstr "通知履歴"
|
||||||
|
|
||||||
#: src/components/alerts/alert-button.tsx
|
#: src/components/alerts/alert-button.tsx
|
||||||
#: src/components/alerts/alerts-sheet.tsx
|
#: src/components/alerts/alerts-sheet.tsx
|
||||||
@@ -406,15 +406,15 @@ msgstr "摂氏 (°C)"
|
|||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Certificate"
|
msgid "Certificate"
|
||||||
msgstr ""
|
msgstr "証明書"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expired {expires}"
|
msgid "Certificate expired {expires}"
|
||||||
msgstr ""
|
msgstr "証明書は{expires}に期限が切れました"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expires {expires}"
|
msgid "Certificate expires {expires}"
|
||||||
msgstr ""
|
msgstr "証明書は{expires}に期限が切れます"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
@@ -435,7 +435,7 @@ msgstr "充電中"
|
|||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Chart options"
|
msgid "Chart options"
|
||||||
msgstr "チャートオプション"
|
msgstr "チャートのオプション"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "Chart width"
|
msgid "Chart width"
|
||||||
@@ -476,7 +476,7 @@ msgstr "すべてクリア"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Clear matches"
|
msgid "Clear matches"
|
||||||
msgstr "一致をクリア"
|
msgstr "一致する項目をクリア"
|
||||||
|
|
||||||
#: src/components/containers-table/containers-table.tsx
|
#: src/components/containers-table/containers-table.tsx
|
||||||
msgid "Click on a container to view more information."
|
msgid "Click on a container to view more information."
|
||||||
@@ -905,7 +905,7 @@ msgstr "アクティブ状態で終了"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr "期限切れ"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
@@ -1019,7 +1019,7 @@ msgstr "FreeBSD コマンド"
|
|||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
#: src/lib/i18n.ts
|
#: src/lib/i18n.ts
|
||||||
msgid "Full"
|
msgid "Full"
|
||||||
msgstr "満充電"
|
msgstr "フル"
|
||||||
|
|
||||||
#. Context: General settings
|
#. Context: General settings
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
@@ -1180,7 +1180,7 @@ msgstr "負荷平均 (5分)"
|
|||||||
#. Short label for load average
|
#. Short label for load average
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Load Avg"
|
msgid "Load Avg"
|
||||||
msgstr "負荷平均"
|
msgstr "平均負荷"
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Load state"
|
msgid "Load state"
|
||||||
@@ -1977,7 +1977,7 @@ msgstr "対象"
|
|||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "target[,protocol[,port[,interval]]]"
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
msgstr ""
|
msgstr "target[,protocol[,port[,interval]]]"
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
@@ -2155,7 +2155,7 @@ msgstr "1時間のパケットロス率がしきい値を超えるとトリガ
|
|||||||
|
|
||||||
#: src/lib/alerts.ts
|
#: src/lib/alerts.ts
|
||||||
msgid "Triggers when status switches between up and down"
|
msgid "Triggers when status switches between up and down"
|
||||||
msgstr "ステータスが稼働中と停止中の間で切り替わったときにトリガーされます"
|
msgstr "システムが開始または停止したときトリガーされます"
|
||||||
|
|
||||||
#: src/lib/alerts.ts
|
#: src/lib/alerts.ts
|
||||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||||
@@ -2291,7 +2291,7 @@ msgstr "表示列"
|
|||||||
|
|
||||||
#: src/components/routes/system/chart-card.tsx
|
#: src/components/routes/system/chart-card.tsx
|
||||||
msgid "Waiting for enough records to display"
|
msgid "Waiting for enough records to display"
|
||||||
msgstr "表示するのに十分なレコードを待っています"
|
msgstr "表示に必要なデータが揃うまで待機中"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "はい"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "ユーザー設定が更新されました。"
|
msgstr "ユーザー設定が更新されました。"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ko\n"
|
"Language: ko\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Korean\n"
|
"Language-Team: Korean\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "예"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "사용자 설정이 업데이트되었습니다."
|
msgstr "사용자 설정이 업데이트되었습니다."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: nl\n"
|
"Language: nl\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Dutch\n"
|
"Language-Team: Dutch\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,11 @@ msgstr "Ja"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
||||||
|
|
||||||
|
#: src/components/logs-display.tsx
|
||||||
|
msgid "Show timestamps"
|
||||||
|
msgstr "Tijdstempels tonen"
|
||||||
|
|
||||||
|
#: src/components/logs-display.tsx
|
||||||
|
msgid "Hide timestamps"
|
||||||
|
msgstr "Tijdstempels verbergen"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: no\n"
|
"Language: no\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 18:40\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Norwegian\n"
|
"Language-Team: Norwegian\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -54,7 +54,7 @@ msgstr "{count, plural, one {{countString} minutt} other {{countString} minutter
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
msgstr ""
|
msgstr "{daysLeft, plural, one {# dag} other {# dager}}"
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
@@ -62,7 +62,7 @@ msgstr "I/O for {diskName}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
msgstr ""
|
msgstr "{numFailed, plural, one {# feilet tjeneste} other {# feilede tjenester}}"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
@@ -70,7 +70,7 @@ msgstr "{threads, plural, one {# tråd} other {# tråder}}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{totalCount, plural, one {# service} other {# services}}"
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
msgstr ""
|
msgstr "{totalCount, plural, one {# tjeneste} other {# tjenester}}"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
@@ -137,7 +137,7 @@ msgstr "Aktiv tilstand"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr "Legg til"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -406,15 +406,15 @@ msgstr "Celsius (°C)"
|
|||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Certificate"
|
msgid "Certificate"
|
||||||
msgstr ""
|
msgstr "Sertifikat"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expired {expires}"
|
msgid "Certificate expired {expires}"
|
||||||
msgstr ""
|
msgstr "Sertifikatet utløper {expires}"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expires {expires}"
|
msgid "Certificate expires {expires}"
|
||||||
msgstr ""
|
msgstr "Sertifikatet utløper {expires}"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
@@ -905,7 +905,7 @@ msgstr "Avsluttet aktiv"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr "Utløpt"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
@@ -1977,7 +1977,7 @@ msgstr "Mål"
|
|||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "target[,protocol[,port[,interval]]]"
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
msgstr ""
|
msgstr "mål [,protokoll[,port[,interval]]"
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ja"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: pl\n"
|
"Language: pl\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Polish\n"
|
"Language-Team: Polish\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Tak"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: pt\n"
|
"Language: pt\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Portuguese\n"
|
"Language-Team: Portuguese\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Sim"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "As configurações do seu usuário foram atualizadas."
|
msgstr "As configurações do seu usuário foram atualizadas."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ro\n"
|
"Language: ro\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-18 19:22\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Romanian\n"
|
"Language-Team: Romanian\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
|
||||||
@@ -52,14 +52,26 @@ msgstr ""
|
|||||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
msgstr "1 oră"
|
msgstr "1 oră"
|
||||||
@@ -123,10 +135,10 @@ msgstr "Alerte Active"
|
|||||||
msgid "Active state"
|
msgid "Active state"
|
||||||
msgstr "Stare activă"
|
msgstr "Stare activă"
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/add-system.tsx
|
msgid "Add"
|
||||||
#: src/components/navbar.tsx
|
msgstr ""
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -136,6 +148,13 @@ msgstr "Stare activă"
|
|||||||
msgid "Add {foo}"
|
msgid "Add {foo}"
|
||||||
msgstr "Adaugă {foo}"
|
msgstr "Adaugă {foo}"
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
msgid "Add System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/notifications.tsx
|
#: src/components/routes/settings/notifications.tsx
|
||||||
msgid "Add URL"
|
msgid "Add URL"
|
||||||
msgstr "Adaugă URL"
|
msgstr "Adaugă URL"
|
||||||
@@ -260,6 +279,7 @@ msgstr ""
|
|||||||
msgid "Average, minimum, and maximum response time"
|
msgid "Average, minimum, and maximum response time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Avg 1h"
|
msgid "Avg 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -383,6 +403,19 @@ msgstr "Atenție - posibilă pierdere de date"
|
|||||||
msgid "Celsius (°C)"
|
msgid "Celsius (°C)"
|
||||||
msgstr "Celsius (°C)"
|
msgstr "Celsius (°C)"
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Certificate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expired {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expires {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
msgstr "Schimbă unitățile de afișare pentru metrici."
|
msgstr "Schimbă unitățile de afișare pentru metrici."
|
||||||
@@ -776,12 +809,15 @@ msgstr "Durată"
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Editează"
|
msgstr "Editează"
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/routes/settings/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
msgid "Edit {foo}"
|
msgid "Edit {foo}"
|
||||||
msgstr "Editează {foo}"
|
msgstr "Editează {foo}"
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Edit System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
#: src/components/login/forgot-pass-form.tsx
|
#: src/components/login/forgot-pass-form.tsx
|
||||||
#: src/components/login/otp-forms.tsx
|
#: src/components/login/otp-forms.tsx
|
||||||
@@ -867,6 +903,10 @@ msgstr ""
|
|||||||
msgid "Exited active"
|
msgid "Exited active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -887,10 +927,6 @@ msgstr ""
|
|||||||
msgid "Fahrenheit (°F)"
|
msgid "Fahrenheit (°F)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
|
||||||
msgid "Failed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
msgid "Failed Attributes:"
|
msgid "Failed Attributes:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1090,6 +1126,7 @@ msgstr ""
|
|||||||
msgid "Inactive"
|
msgid "Inactive"
|
||||||
msgstr "Inactiv"
|
msgstr "Inactiv"
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/settings/heartbeat.tsx
|
#: src/components/routes/settings/heartbeat.tsx
|
||||||
msgid "Interval"
|
msgid "Interval"
|
||||||
@@ -1182,6 +1219,7 @@ msgctxt "Packet loss"
|
|||||||
msgid "Loss"
|
msgid "Loss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Loss 1h"
|
msgid "Loss 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1204,6 +1242,7 @@ msgstr ""
|
|||||||
msgid "Max 1 min"
|
msgid "Max 1 min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Max 1h"
|
msgid "Max 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1234,6 +1273,7 @@ msgstr "Memorie Utilizată"
|
|||||||
msgid "Memory usage of containers"
|
msgid "Memory usage of containers"
|
||||||
msgstr "Utilizarea memoriei de către containere"
|
msgstr "Utilizarea memoriei de către containere"
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Min 1h"
|
msgid "Min 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1565,6 +1605,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Protocol"
|
msgid "Protocol"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1653,6 +1694,7 @@ msgstr ""
|
|||||||
msgid "Resolved"
|
msgid "Resolved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||||
msgid "Response"
|
msgid "Response"
|
||||||
@@ -1698,7 +1740,6 @@ msgstr "Detalii S.M.A.R.T."
|
|||||||
msgid "S.M.A.R.T. Self-Test"
|
msgid "S.M.A.R.T. Self-Test"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Save {foo}"
|
msgid "Save {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1712,6 +1753,10 @@ msgstr ""
|
|||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Save System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Saved in the database and does not expire until you disable it."
|
msgid "Saved in the database and does not expire until you disable it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1833,6 +1878,7 @@ msgstr ""
|
|||||||
msgid "SMTP settings"
|
msgid "SMTP settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Sort By"
|
msgid "Sort By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1875,12 +1921,11 @@ msgstr ""
|
|||||||
msgid "Switch theme"
|
msgid "Switch theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-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/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
@@ -1925,9 +1970,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Target"
|
msgid "Target"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
msgstr "Sarcini"
|
msgstr "Sarcini"
|
||||||
@@ -2162,6 +2213,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
#: src/components/routes/system/storage-pools-table.tsx
|
#: src/components/routes/system/storage-pools-table.tsx
|
||||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||||
@@ -2217,6 +2269,7 @@ msgstr ""
|
|||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2231,6 +2284,7 @@ msgstr ""
|
|||||||
msgid "View your 200 most recent alerts."
|
msgid "View your 200 most recent alerts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Visible Fields"
|
msgid "Visible Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-26 03:21\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Russian\n"
|
"Language-Team: Russian\n"
|
||||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||||
@@ -54,7 +54,7 @@ msgstr "{count, plural, one {{countString} минута} few {{countString} ми
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
msgstr ""
|
msgstr "{daysLeft, plural, one {# день} other {# дней}}"
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
@@ -62,7 +62,7 @@ msgstr "Ввод-вывод {diskName}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
msgstr ""
|
msgstr "{numFailed, plural, one {# сбой службы} other {# сбоев служб}}"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
@@ -70,7 +70,7 @@ msgstr "{threads, plural, one {# поток} few {# потока} many {# пот
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{totalCount, plural, one {# service} other {# services}}"
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
msgstr ""
|
msgstr "{totalCount, plural, one {# сервис} other {# сервисов}}"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
@@ -137,7 +137,7 @@ msgstr "Активное состояние"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr "Добавить"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -406,15 +406,15 @@ msgstr "Цельсий (°C)"
|
|||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Certificate"
|
msgid "Certificate"
|
||||||
msgstr ""
|
msgstr "Сертификат"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expired {expires}"
|
msgid "Certificate expired {expires}"
|
||||||
msgstr ""
|
msgstr "Сертификат истёк {expires}"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expires {expires}"
|
msgid "Certificate expires {expires}"
|
||||||
msgstr ""
|
msgstr "Сертификат истекает {expires}"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
@@ -905,7 +905,7 @@ msgstr "Завершился активным"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr "Срок истек"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
@@ -1977,7 +1977,7 @@ msgstr "Цель"
|
|||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "target[,protocol[,port[,interval]]]"
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
msgstr ""
|
msgstr "target[,protocol[,port[,интервал]]]"
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Да"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Ваши настройки пользователя были обновлены."
|
msgstr "Ваши настройки пользователя были обновлены."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: sl\n"
|
"Language: sl\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Slovenian\n"
|
"Language-Team: Slovenian\n"
|
||||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Da"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: sr\n"
|
"Language: sr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Serbian (Cyrillic)\n"
|
"Language-Team: Serbian (Cyrillic)\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Да"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Ваша корисничка подешавања су ажурирана."
|
msgstr "Ваша корисничка подешавања су ажурирана."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: sv\n"
|
"Language: sv\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Swedish\n"
|
"Language-Team: Swedish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ja"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Dina användarinställningar har uppdaterats."
|
msgstr "Dina användarinställningar har uppdaterats."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: th\n"
|
"Language: th\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-18 19:22\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Thai\n"
|
"Language-Team: Thai\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -52,14 +52,26 @@ msgstr ""
|
|||||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -123,10 +135,10 @@ msgstr ""
|
|||||||
msgid "Active state"
|
msgid "Active state"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/add-system.tsx
|
msgid "Add"
|
||||||
#: src/components/navbar.tsx
|
msgstr ""
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -136,6 +148,13 @@ msgstr ""
|
|||||||
msgid "Add {foo}"
|
msgid "Add {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
msgid "Add System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/notifications.tsx
|
#: src/components/routes/settings/notifications.tsx
|
||||||
msgid "Add URL"
|
msgid "Add URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -260,6 +279,7 @@ msgstr ""
|
|||||||
msgid "Average, minimum, and maximum response time"
|
msgid "Average, minimum, and maximum response time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Avg 1h"
|
msgid "Avg 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -383,6 +403,19 @@ msgstr ""
|
|||||||
msgid "Celsius (°C)"
|
msgid "Celsius (°C)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Certificate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expired {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expires {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -776,12 +809,15 @@ msgstr ""
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/routes/settings/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
msgid "Edit {foo}"
|
msgid "Edit {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Edit System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
#: src/components/login/forgot-pass-form.tsx
|
#: src/components/login/forgot-pass-form.tsx
|
||||||
#: src/components/login/otp-forms.tsx
|
#: src/components/login/otp-forms.tsx
|
||||||
@@ -867,6 +903,10 @@ msgstr ""
|
|||||||
msgid "Exited active"
|
msgid "Exited active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -887,10 +927,6 @@ msgstr ""
|
|||||||
msgid "Fahrenheit (°F)"
|
msgid "Fahrenheit (°F)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
|
||||||
msgid "Failed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
msgid "Failed Attributes:"
|
msgid "Failed Attributes:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1090,6 +1126,7 @@ msgstr ""
|
|||||||
msgid "Inactive"
|
msgid "Inactive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/settings/heartbeat.tsx
|
#: src/components/routes/settings/heartbeat.tsx
|
||||||
msgid "Interval"
|
msgid "Interval"
|
||||||
@@ -1182,6 +1219,7 @@ msgctxt "Packet loss"
|
|||||||
msgid "Loss"
|
msgid "Loss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Loss 1h"
|
msgid "Loss 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1204,6 +1242,7 @@ msgstr ""
|
|||||||
msgid "Max 1 min"
|
msgid "Max 1 min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Max 1h"
|
msgid "Max 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1234,6 +1273,7 @@ msgstr ""
|
|||||||
msgid "Memory usage of containers"
|
msgid "Memory usage of containers"
|
||||||
msgstr "การใช้หน่วยความจำของคอนเทนเนอร์"
|
msgstr "การใช้หน่วยความจำของคอนเทนเนอร์"
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Min 1h"
|
msgid "Min 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1565,6 +1605,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Protocol"
|
msgid "Protocol"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1653,6 +1694,7 @@ msgstr ""
|
|||||||
msgid "Resolved"
|
msgid "Resolved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||||
msgid "Response"
|
msgid "Response"
|
||||||
@@ -1698,7 +1740,6 @@ msgstr ""
|
|||||||
msgid "S.M.A.R.T. Self-Test"
|
msgid "S.M.A.R.T. Self-Test"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Save {foo}"
|
msgid "Save {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1712,6 +1753,10 @@ msgstr ""
|
|||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Save System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Saved in the database and does not expire until you disable it."
|
msgid "Saved in the database and does not expire until you disable it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1833,6 +1878,7 @@ msgstr ""
|
|||||||
msgid "SMTP settings"
|
msgid "SMTP settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Sort By"
|
msgid "Sort By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1875,12 +1921,11 @@ msgstr ""
|
|||||||
msgid "Switch theme"
|
msgid "Switch theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-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/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
@@ -1925,9 +1970,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Target"
|
msgid "Target"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2162,6 +2213,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
#: src/components/routes/system/storage-pools-table.tsx
|
#: src/components/routes/system/storage-pools-table.tsx
|
||||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||||
@@ -2217,6 +2269,7 @@ msgstr ""
|
|||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2231,6 +2284,7 @@ msgstr ""
|
|||||||
msgid "View your 200 most recent alerts."
|
msgid "View your 200 most recent alerts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Visible Fields"
|
msgid "Visible Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: tr\n"
|
"Language: tr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Turkish\n"
|
"Language-Team: Turkish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Evet"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Kullanıcı ayarlarınız güncellendi."
|
msgstr "Kullanıcı ayarlarınız güncellendi."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: ug\n"
|
"Language: ug\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-18 19:22\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Uyghur\n"
|
"Language-Team: Uyghur\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -52,14 +52,26 @@ msgstr ""
|
|||||||
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
msgid "{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -123,10 +135,10 @@ msgstr ""
|
|||||||
msgid "Active state"
|
msgid "Active state"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/add-system.tsx
|
msgid "Add"
|
||||||
#: src/components/navbar.tsx
|
msgstr ""
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -136,6 +148,13 @@ msgstr ""
|
|||||||
msgid "Add {foo}"
|
msgid "Add {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
#: src/components/navbar.tsx
|
||||||
|
msgid "Add System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/notifications.tsx
|
#: src/components/routes/settings/notifications.tsx
|
||||||
msgid "Add URL"
|
msgid "Add URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -260,6 +279,7 @@ msgstr ""
|
|||||||
msgid "Average, minimum, and maximum response time"
|
msgid "Average, minimum, and maximum response time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Avg 1h"
|
msgid "Avg 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -383,6 +403,19 @@ msgstr ""
|
|||||||
msgid "Celsius (°C)"
|
msgid "Celsius (°C)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Certificate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expired {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
|
msgid "Certificate expires {expires}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -776,12 +809,15 @@ msgstr ""
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/routes/settings/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
msgid "Edit {foo}"
|
msgid "Edit {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Edit System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/login/auth-form.tsx
|
#: src/components/login/auth-form.tsx
|
||||||
#: src/components/login/forgot-pass-form.tsx
|
#: src/components/login/forgot-pass-form.tsx
|
||||||
#: src/components/login/otp-forms.tsx
|
#: src/components/login/otp-forms.tsx
|
||||||
@@ -867,6 +903,10 @@ msgstr ""
|
|||||||
msgid "Exited active"
|
msgid "Exited active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
msgid "Expired"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -887,10 +927,6 @@ msgstr ""
|
|||||||
msgid "Fahrenheit (°F)"
|
msgid "Fahrenheit (°F)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
|
||||||
msgid "Failed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
msgid "Failed Attributes:"
|
msgid "Failed Attributes:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1090,6 +1126,7 @@ msgstr ""
|
|||||||
msgid "Inactive"
|
msgid "Inactive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/settings/heartbeat.tsx
|
#: src/components/routes/settings/heartbeat.tsx
|
||||||
msgid "Interval"
|
msgid "Interval"
|
||||||
@@ -1182,6 +1219,7 @@ msgctxt "Packet loss"
|
|||||||
msgid "Loss"
|
msgid "Loss"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Loss 1h"
|
msgid "Loss 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1204,6 +1242,7 @@ msgstr ""
|
|||||||
msgid "Max 1 min"
|
msgid "Max 1 min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Max 1h"
|
msgid "Max 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1234,6 +1273,7 @@ msgstr ""
|
|||||||
msgid "Memory usage of containers"
|
msgid "Memory usage of containers"
|
||||||
msgstr "كونتېينېرلارنىڭ ئەسلەك ئىشلىتىشى"
|
msgstr "كونتېينېرلارنىڭ ئەسلەك ئىشلىتىشى"
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Min 1h"
|
msgid "Min 1h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1565,6 +1605,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Protocol"
|
msgid "Protocol"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1653,6 +1694,7 @@ msgstr ""
|
|||||||
msgid "Resolved"
|
msgid "Resolved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||||
msgid "Response"
|
msgid "Response"
|
||||||
@@ -1698,7 +1740,6 @@ msgstr ""
|
|||||||
msgid "S.M.A.R.T. Self-Test"
|
msgid "S.M.A.R.T. Self-Test"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Save {foo}"
|
msgid "Save {foo}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1712,6 +1753,10 @@ msgstr ""
|
|||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/add-system.tsx
|
||||||
|
msgid "Save System"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Saved in the database and does not expire until you disable it."
|
msgid "Saved in the database and does not expire until you disable it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1833,6 +1878,7 @@ msgstr ""
|
|||||||
msgid "SMTP settings"
|
msgid "SMTP settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Sort By"
|
msgid "Sort By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1875,12 +1921,11 @@ msgstr ""
|
|||||||
msgid "Switch theme"
|
msgid "Switch theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/add-system.tsx
|
|
||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/navbar.tsx
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-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/quiet-hours.tsx
|
#: src/components/routes/settings/quiet-hours.tsx
|
||||||
@@ -1925,9 +1970,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Target"
|
msgid "Target"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2162,6 +2213,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
#: src/components/routes/system/storage-pools-table.tsx
|
#: src/components/routes/system/storage-pools-table.tsx
|
||||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||||
@@ -2217,6 +2269,7 @@ msgstr ""
|
|||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2231,6 +2284,7 @@ msgstr ""
|
|||||||
msgid "View your 200 most recent alerts."
|
msgid "View your 200 most recent alerts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
#: src/components/systems-table/systems-table.tsx
|
#: src/components/systems-table/systems-table.tsx
|
||||||
msgid "Visible Fields"
|
msgid "Visible Fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: uk\n"
|
"Language: uk\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Ukrainian\n"
|
"Language-Team: Ukrainian\n"
|
||||||
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Так"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Ваші налаштування користувача були оновлені."
|
msgstr "Ваші налаштування користувача були оновлені."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: uz\n"
|
"Language: uz\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Uzbek\n"
|
"Language-Team: Uzbek\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Ha"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Foydalanuvchi sozlamalaringiz yangilandi."
|
msgstr "Foydalanuvchi sozlamalaringiz yangilandi."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: vi\n"
|
"Language: vi\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Vietnamese\n"
|
"Language-Team: Vietnamese\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "Có"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: zh\n"
|
"Language: zh\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-25 07:01\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Simplified\n"
|
"Language-Team: Chinese Simplified\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -54,7 +54,7 @@ msgstr "{count, plural, one {{countString} 分钟} few {{countString} 分钟} ma
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
msgid "{daysLeft, plural, one {# day} other {# days}}"
|
||||||
msgstr ""
|
msgstr "{daysLeft, plural, other {# 天}}"
|
||||||
|
|
||||||
#: src/components/routes/system/charts/disk-charts.tsx
|
#: src/components/routes/system/charts/disk-charts.tsx
|
||||||
msgid "{diskName} I/O"
|
msgid "{diskName} I/O"
|
||||||
@@ -62,7 +62,7 @@ msgstr "{diskName} I/O"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
msgid "{numFailed, plural, one {# failed service} other {# failed services}}"
|
||||||
msgstr ""
|
msgstr "{numFailed, plural, other {# 个失败服务}}"
|
||||||
|
|
||||||
#: src/components/routes/system/info-bar.tsx
|
#: src/components/routes/system/info-bar.tsx
|
||||||
msgid "{threads, plural, one {# thread} other {# threads}}"
|
msgid "{threads, plural, one {# thread} other {# threads}}"
|
||||||
@@ -70,7 +70,7 @@ msgstr "{threads, plural, one {# 线程} other {# 线程}}"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "{totalCount, plural, one {# service} other {# services}}"
|
msgid "{totalCount, plural, one {# service} other {# services}}"
|
||||||
msgstr ""
|
msgstr "{totalCount, plural, other {# 个服务}}"
|
||||||
|
|
||||||
#: src/lib/utils.ts
|
#: src/lib/utils.ts
|
||||||
msgid "1 hour"
|
msgid "1 hour"
|
||||||
@@ -137,7 +137,7 @@ msgstr "活动状态"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr ""
|
msgstr "添加"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
@@ -406,15 +406,15 @@ msgstr "摄氏度 (°C)"
|
|||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Certificate"
|
msgid "Certificate"
|
||||||
msgstr ""
|
msgstr "证书"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expired {expires}"
|
msgid "Certificate expired {expires}"
|
||||||
msgstr ""
|
msgstr "证书已过期 {expires}"
|
||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||||
msgid "Certificate expires {expires}"
|
msgid "Certificate expires {expires}"
|
||||||
msgstr ""
|
msgstr "证书将于 {expires} 过期"
|
||||||
|
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
msgid "Change display units for metrics."
|
msgid "Change display units for metrics."
|
||||||
@@ -905,7 +905,7 @@ msgstr "退出活动状态"
|
|||||||
|
|
||||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr "已过期"
|
||||||
|
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Expires after one hour or on hub restart."
|
msgid "Expires after one hour or on hub restart."
|
||||||
@@ -1977,7 +1977,7 @@ msgstr "目标"
|
|||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||||
msgid "target[,protocol[,port[,interval]]]"
|
msgid "target[,protocol[,port[,interval]]]"
|
||||||
msgstr ""
|
msgstr "目标[,协议[,端口[,间隔]]]"
|
||||||
|
|
||||||
#: src/components/systemd-table/systemd-table.tsx
|
#: src/components/systemd-table/systemd-table.tsx
|
||||||
msgid "Tasks"
|
msgid "Tasks"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "是"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "您的用户设置已更新。"
|
msgstr "您的用户设置已更新。"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: zh\n"
|
"Language: zh\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:16\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Traditional, Hong Kong\n"
|
"Language-Team: Chinese Traditional, Hong Kong\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "是"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "您的用戶設置已更新。"
|
msgstr "您的用戶設置已更新。"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: zh\n"
|
"Language: zh\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2026-09-24 16:15\n"
|
"PO-Revision-Date: 2026-09-24 17:29\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Traditional\n"
|
"Language-Team: Chinese Traditional\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
@@ -2359,3 +2359,4 @@ msgstr "是"
|
|||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
msgid "Your user settings have been updated."
|
msgid "Your user settings have been updated."
|
||||||
msgstr "已更新您的使用者設定"
|
msgstr "已更新您的使用者設定"
|
||||||
|
|
||||||
|
|||||||
Vendored
+14
@@ -459,6 +459,20 @@ export interface AlertInfo {
|
|||||||
/** Additional information that remains visible while the alert is enabled */
|
/** Additional information that remains visible while the alert is enabled */
|
||||||
note?: () => string
|
note?: () => string
|
||||||
invert?: boolean
|
invert?: boolean
|
||||||
|
/** Selectable threshold units. Values are stored in the first unit (factor 1) */
|
||||||
|
units?: AlertUnit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertUnit {
|
||||||
|
/** Unit suffix shown after the value */
|
||||||
|
unit: string
|
||||||
|
/** Multiplier converting a value in this unit to the stored value */
|
||||||
|
factor: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
step: number
|
||||||
|
/** Finer step for the number input, which also accepts values down to this step */
|
||||||
|
inputStep?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
||||||
|
|||||||
Reference in New Issue
Block a user