mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
Compare commits
24 Commits
v0.12.0-be
...
261f7fb76c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
261f7fb76c | ||
|
|
18d9258907 | ||
|
|
9d7fb8ab80 | ||
|
|
3730a78e5a | ||
|
|
7cdd0907e8 | ||
|
|
3586f73f30 | ||
|
|
752ccc6beb | ||
|
|
f577476c81 | ||
|
|
49ae424698 | ||
|
|
d4fd19522b | ||
|
|
5c047e4afd | ||
|
|
6576141f54 | ||
|
|
926e807020 | ||
|
|
d91847c6c5 | ||
|
|
0abd88270c | ||
|
|
806c4e51c5 | ||
|
|
6520783fe9 | ||
|
|
48c8a3a4a5 | ||
|
|
e0c839f78c | ||
|
|
1ba362bafe | ||
|
|
b5d55ead4a | ||
|
|
4f879ccc66 | ||
|
|
cd9e0f7b5b | ||
|
|
780644eeae |
47
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
47
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,19 @@
|
||||
name: 🐛 Bug report
|
||||
description: Report a new bug or issue.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
labels: ['bug', "needs confirmation"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of Beszel is this about?
|
||||
options:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
@@ -43,6 +54,39 @@ body:
|
||||
3. Pour it into a cup.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Which category does this relate to most?
|
||||
options:
|
||||
- Metrics
|
||||
- Charts & Visualization
|
||||
- Settings & Configuration
|
||||
- Notifications & Alerts
|
||||
- Authentication
|
||||
- Installation
|
||||
- Performance
|
||||
- UI / UX
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Affected Metrics
|
||||
description: If applicable, which specific metric does this relate to most?
|
||||
options:
|
||||
- CPU
|
||||
- Memory
|
||||
- Storage
|
||||
- Network
|
||||
- Containers
|
||||
- GPU
|
||||
- Sensors
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
@@ -61,7 +105,6 @@ body:
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation method
|
||||
default: 0
|
||||
options:
|
||||
- Docker
|
||||
- Binary
|
||||
|
||||
60
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
60
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,8 +1,19 @@
|
||||
name: 🚀 Feature request
|
||||
description: Request a new feature or change.
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
labels: ["enhancement", "needs review"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of Beszel is this about?
|
||||
options:
|
||||
- Hub
|
||||
- Agent
|
||||
- Hub & Agent
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting, please search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
@@ -11,8 +22,55 @@ body:
|
||||
label: Describe the feature you would like to see
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: Motivation / Use Case
|
||||
description: Why do you want this feature? What problem does it solve?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe how you would like to see this feature implemented
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please attach any relevant screenshots, such as images from your current solution or similar implementations.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Which category does this relate to most?
|
||||
options:
|
||||
- Metrics
|
||||
- Charts & Visualization
|
||||
- Settings & Configuration
|
||||
- Notifications & Alerts
|
||||
- Authentication
|
||||
- Installation
|
||||
- Performance
|
||||
- UI / UX
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Affected Metrics
|
||||
description: If applicable, which specific metric does this relate to most?
|
||||
options:
|
||||
- CPU
|
||||
- Memory
|
||||
- Storage
|
||||
- Network
|
||||
- Containers
|
||||
- GPU
|
||||
- Sensors
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
## 📃 Description
|
||||
|
||||
A short description of the pull request changes should go here and the sections below should list in detail all changes. You can remove the sections you don't need.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
Add a link to the PR for [documentation](https://github.com/henrygd/beszel-docs) changes.
|
||||
|
||||
## 🪵 Changelog
|
||||
|
||||
### ➕ Added
|
||||
|
||||
- one
|
||||
- two
|
||||
|
||||
### ✏️ Changed
|
||||
|
||||
- one
|
||||
- two
|
||||
|
||||
### 🔧 Fixed
|
||||
|
||||
- one
|
||||
- two
|
||||
|
||||
### 🗑️ Removed
|
||||
|
||||
- one
|
||||
- two
|
||||
|
||||
## 📷 Screenshots
|
||||
|
||||
If this PR has any UI/UX changes it's strongly suggested you add screenshots here.
|
||||
2
.github/workflows/docker-images.yml
vendored
2
.github/workflows/docker-images.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
tags: |
|
||||
type=edge,enable=true
|
||||
type=raw,value=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
43
.github/workflows/inactivity-actions.yml
vendored
Normal file
43
.github/workflows/inactivity-actions.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: 'Issue and PR Maintenance'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # runs at midnight UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
close-stale:
|
||||
name: Close Stale Issues
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Close Stale Issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Messaging
|
||||
stale-issue-message: >
|
||||
👋 This issue has been automatically marked as stale due to inactivity.
|
||||
If this issue is still relevant, please comment to keep it open.
|
||||
Without activity, it will be closed in 7 days.
|
||||
|
||||
close-issue-message: >
|
||||
🔒 This issue has been automatically closed due to prolonged inactivity.
|
||||
Feel free to open a new issue if you have further questions or concerns.
|
||||
|
||||
# Timing
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 7
|
||||
|
||||
# Labels
|
||||
stale-issue-label: 'stale'
|
||||
remove-stale-when-updated: true
|
||||
only-issue-labels: 'awaiting-requester'
|
||||
|
||||
# Exemptions
|
||||
exempt-assignees: true
|
||||
exempt-milestones: true
|
||||
82
.github/workflows/label-from-dropdown.yml
vendored
Normal file
82
.github/workflows/label-from-dropdown.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Label issues from dropdowns
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
label_from_dropdown:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Apply labels based on dropdown choices
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Get the issue body
|
||||
const body = context.payload.issue.body;
|
||||
|
||||
// Helper to find dropdown value in the body (assuming markdown format)
|
||||
function extractSectionValue(heading) {
|
||||
const regex = new RegExp(`### ${heading}\\s+([\\s\\S]*?)(?:\\n###|$)`, 'i');
|
||||
const match = body.match(regex);
|
||||
if (match) {
|
||||
// Get the first non-empty line after the heading
|
||||
const lines = match[1].split('\n').map(l => l.trim()).filter(Boolean);
|
||||
return lines[0] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract dropdown selections
|
||||
const category = extractSectionValue('Category');
|
||||
const metrics = extractSectionValue('Affected Metrics');
|
||||
const component = extractSectionValue('Component');
|
||||
|
||||
// Build labels to add
|
||||
let labelsToAdd = [];
|
||||
if (category) labelsToAdd.push(category);
|
||||
if (metrics) labelsToAdd.push(metrics);
|
||||
if (component) labelsToAdd.push(component);
|
||||
|
||||
// Get existing labels in the repo
|
||||
const { data: existingLabels } = await github.rest.issues.listLabelsForRepo({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100
|
||||
});
|
||||
const existingLabelNames = existingLabels.map(l => l.name);
|
||||
|
||||
// Find labels that need to be created
|
||||
const labelsToCreate = labelsToAdd.filter(label => !existingLabelNames.includes(label));
|
||||
|
||||
// Create missing labels (with a default color)
|
||||
for (const label of labelsToCreate) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: label,
|
||||
color: 'ededed' // light gray, you can pick any hex color
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore if label already exists (race condition), otherwise rethrow
|
||||
if (!e || e.status !== 422) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply all labels (they all exist now)
|
||||
if (labelsToAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: labelsToAdd
|
||||
});
|
||||
}
|
||||
@@ -119,8 +119,8 @@ scoops:
|
||||
repository:
|
||||
owner: henrygd
|
||||
name: beszel-scoops
|
||||
homepage: 'https://beszel.dev'
|
||||
description: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
homepage: "https://beszel.dev"
|
||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
license: MIT
|
||||
|
||||
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
|
||||
@@ -152,9 +152,10 @@ brews:
|
||||
repository:
|
||||
owner: henrygd
|
||||
name: homebrew-beszel
|
||||
homepage: 'https://beszel.dev'
|
||||
description: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
homepage: "https://beszel.dev"
|
||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
license: MIT
|
||||
skip_upload: auto
|
||||
extra_install: |
|
||||
(bin/"beszel-agent-launcher").write <<~EOS
|
||||
#!/bin/bash
|
||||
@@ -181,12 +182,12 @@ winget:
|
||||
package_identifier: henrygd.beszel-agent
|
||||
publisher: henrygd
|
||||
license: MIT
|
||||
license_url: 'https://github.com/henrygd/beszel/blob/main/LICENSE'
|
||||
copyright: '2025 henrygd'
|
||||
homepage: 'https://beszel.dev'
|
||||
release_notes_url: 'https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}'
|
||||
publisher_support_url: 'https://github.com/henrygd/beszel/issues'
|
||||
short_description: 'Agent for Beszel, a lightweight server monitoring platform.'
|
||||
license_url: "https://github.com/henrygd/beszel/blob/main/LICENSE"
|
||||
copyright: "2025 henrygd"
|
||||
homepage: "https://beszel.dev"
|
||||
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
|
||||
publisher_support_url: "https://github.com/henrygd/beszel/issues"
|
||||
short_description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
skip_upload: auto
|
||||
description: |
|
||||
Beszel is a lightweight server monitoring platform that includes Docker
|
||||
@@ -218,5 +219,5 @@ changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
@@ -56,7 +56,7 @@ dev-hub: export ENV=dev
|
||||
dev-hub:
|
||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./cmd/hub ./internal/{alerts,hub,records,users} -name "*.go" | entr -r -s "cd ./cmd/hub && go run . serve"; \
|
||||
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
|
||||
else \
|
||||
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
|
||||
fi
|
||||
|
||||
@@ -7,20 +7,20 @@ replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/fxamacker/cbor/v2 v2.8.0
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lxzan/gws v1.8.9
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.15
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.28.4
|
||||
github.com/pocketbase/pocketbase v0.29.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
github.com/spf13/cast v1.9.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||
@@ -52,19 +52,19 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/image v0.28.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
@@ -26,8 +26,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
@@ -46,8 +46,8 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -103,8 +103,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.28.4 h1:RmhWXDcfKrFM9/W0G0Zrlv4eKBM8/s/v4SQKytjgD20=
|
||||
github.com/pocketbase/pocketbase v0.28.4/go.mod h1:jSuN93vE/oeJVOz2D2ZxcYyr2bYNmDOMCUkM+JhyJQ0=
|
||||
github.com/pocketbase/pocketbase v0.29.0 h1:oL6qvkU2QSybClVtQdaq9Z1F3Wk59iKYCfIaf1R8KUs=
|
||||
github.com/pocketbase/pocketbase v0.29.0/go.mod h1:SqyH7o/3e+/uLySATlJqxH4S8gyU6R0adG56ZSV1vuU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -120,8 +120,9 @@ github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
@@ -143,29 +144,29 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -173,19 +174,19 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -201,16 +202,20 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
@@ -103,6 +105,11 @@ func (a *Agent) updateTemperatures(systemStats *system.Stats) {
|
||||
|
||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||
for i, sensor := range temps {
|
||||
// check for malformed strings on darwin (gopsutil/issues/1832)
|
||||
if runtime.GOOS == "darwin" && !utf8.ValidString(sensor.SensorKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
// scale temperature
|
||||
if sensor.Temperature != 0 && sensor.Temperature < 1 {
|
||||
sensor.Temperature = scaleTemperature(sensor.Temperature)
|
||||
|
||||
@@ -475,7 +475,7 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "matching beta version should use CBOR",
|
||||
hubVersion: "0.12.0-beta1",
|
||||
hubVersion: "0.12.0-beta2",
|
||||
expectedUsesCbor: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/shirou/gopsutil/v4/load"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
psutilNet "github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
@@ -77,6 +78,16 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
systemStats.Cpu = twoDecimals(cpuPct[0])
|
||||
}
|
||||
|
||||
// load average
|
||||
if avgstat, err := load.Avg(); err == nil {
|
||||
systemStats.LoadAvg1 = twoDecimals(avgstat.Load1)
|
||||
systemStats.LoadAvg5 = twoDecimals(avgstat.Load5)
|
||||
systemStats.LoadAvg15 = twoDecimals(avgstat.Load15)
|
||||
slog.Debug("Load average", "5m", systemStats.LoadAvg5, "15m", systemStats.LoadAvg15)
|
||||
} else {
|
||||
slog.Error("Error getting load average", "err", err)
|
||||
}
|
||||
|
||||
// memory
|
||||
if v, err := mem.VirtualMemory(); err == nil {
|
||||
// swap
|
||||
@@ -240,6 +251,9 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
|
||||
// update base system info
|
||||
a.systemInfo.Cpu = systemStats.Cpu
|
||||
a.systemInfo.LoadAvg1 = systemStats.LoadAvg1
|
||||
a.systemInfo.LoadAvg5 = systemStats.LoadAvg5
|
||||
a.systemInfo.LoadAvg15 = systemStats.LoadAvg15
|
||||
a.systemInfo.MemPct = systemStats.MemPct
|
||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||
a.systemInfo.Uptime, _ = host.Uptime()
|
||||
|
||||
@@ -47,6 +47,9 @@ type SystemAlertStats struct {
|
||||
NetSent float64 `json:"ns"`
|
||||
NetRecv float64 `json:"nr"`
|
||||
Temperatures map[string]float32 `json:"t"`
|
||||
LoadAvg1 float64 `json:"l1"`
|
||||
LoadAvg5 float64 `json:"l5"`
|
||||
LoadAvg15 float64 `json:"l15"`
|
||||
}
|
||||
|
||||
type SystemAlertData struct {
|
||||
@@ -90,10 +93,18 @@ func NewAlertManager(app hubLike) *AlertManager {
|
||||
alertQueue: make(chan alertTask),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
am.bindEvents()
|
||||
go am.startWorker()
|
||||
return am
|
||||
}
|
||||
|
||||
// Bind events to the alerts collection lifecycle
|
||||
func (am *AlertManager) bindEvents() {
|
||||
am.hub.OnRecordAfterUpdateSuccess("alerts").BindFunc(updateHistoryOnAlertUpdate)
|
||||
am.hub.OnRecordAfterDeleteSuccess("alerts").BindFunc(resolveHistoryOnAlertDelete)
|
||||
}
|
||||
|
||||
// SendAlert sends an alert to the user
|
||||
func (am *AlertManager) SendAlert(data AlertMessageData) error {
|
||||
// get user settings
|
||||
record, err := am.hub.FindFirstRecordByFilter(
|
||||
|
||||
85
beszel/internal/alerts/alerts_history.go
Normal file
85
beszel/internal/alerts/alerts_history.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// On triggered alert record delete, set matching alert history record to resolved
|
||||
func resolveHistoryOnAlertDelete(e *core.RecordEvent) error {
|
||||
if !e.Record.GetBool("triggered") {
|
||||
return e.Next()
|
||||
}
|
||||
_ = resolveAlertHistoryRecord(e.App, e.Record)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// On alert record update, update alert history record
|
||||
func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
|
||||
original := e.Record.Original()
|
||||
new := e.Record
|
||||
|
||||
originalTriggered := original.GetBool("triggered")
|
||||
newTriggered := new.GetBool("triggered")
|
||||
|
||||
// no need to update alert history if triggered state has not changed
|
||||
if originalTriggered == newTriggered {
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// if new state is triggered, create new alert history record
|
||||
if newTriggered {
|
||||
_, _ = createAlertHistoryRecord(e.App, new)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// if new state is not triggered, check for matching alert history record and set it to resolved
|
||||
_ = resolveAlertHistoryRecord(e.App, new)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// resolveAlertHistoryRecord sets the resolved field to the current time
|
||||
func resolveAlertHistoryRecord(app core.App, alertRecord *core.Record) error {
|
||||
alertHistoryRecords, err := app.FindRecordsByFilter(
|
||||
"alerts_history",
|
||||
"alert_id={:alert_id} && resolved=null",
|
||||
"-created",
|
||||
1,
|
||||
0,
|
||||
dbx.Params{"alert_id": alertRecord.Id},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(alertHistoryRecords) == 0 {
|
||||
return nil
|
||||
}
|
||||
alertHistoryRecord := alertHistoryRecords[0] // there should be only one record
|
||||
alertHistoryRecord.Set("resolved", time.Now().UTC())
|
||||
err = app.Save(alertHistoryRecord)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to resolve alert history", "err", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// createAlertHistoryRecord creates a new alert history record
|
||||
func createAlertHistoryRecord(app core.App, alertRecord *core.Record) (alertHistoryRecord *core.Record, err error) {
|
||||
alertHistoryCollection, err := app.FindCachedCollectionByNameOrId("alerts_history")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alertHistoryRecord = core.NewRecord(alertHistoryCollection)
|
||||
alertHistoryRecord.Set("alert_id", alertRecord.Id)
|
||||
alertHistoryRecord.Set("user", alertRecord.GetString("user"))
|
||||
alertHistoryRecord.Set("system", alertRecord.GetString("system"))
|
||||
alertHistoryRecord.Set("name", alertRecord.GetString("name"))
|
||||
alertHistoryRecord.Set("value", alertRecord.GetFloat("value"))
|
||||
err = app.Save(alertHistoryRecord)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to save alert history", "err", err)
|
||||
}
|
||||
return alertHistoryRecord, err
|
||||
}
|
||||
@@ -136,6 +136,14 @@ func (am *AlertManager) handleSystemUp(systemName string, alertRecords []*core.R
|
||||
|
||||
// sendStatusAlert sends a status alert ("up" or "down") to the users associated with the alert records.
|
||||
func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, alertRecord *core.Record) error {
|
||||
switch alertStatus {
|
||||
case "up":
|
||||
alertRecord.Set("triggered", false)
|
||||
case "down":
|
||||
alertRecord.Set("triggered", true)
|
||||
}
|
||||
am.hub.Save(alertRecord)
|
||||
|
||||
var emoji string
|
||||
if alertStatus == "up" {
|
||||
emoji = "\u2705" // Green checkmark emoji
|
||||
@@ -146,16 +154,16 @@ func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, a
|
||||
title := fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji)
|
||||
message := strings.TrimSuffix(title, emoji)
|
||||
|
||||
if errs := am.hub.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
return errs["user"]
|
||||
}
|
||||
user := alertRecord.ExpandedOne("user")
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
// if errs := am.hub.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
// return errs["user"]
|
||||
// }
|
||||
// user := alertRecord.ExpandedOne("user")
|
||||
// if user == nil {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
return am.SendAlert(AlertMessageData{
|
||||
UserID: user.Id,
|
||||
UserID: alertRecord.GetString("user"),
|
||||
Title: title,
|
||||
Message: message,
|
||||
Link: am.hub.MakeLink("system", systemName),
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {
|
||||
alertRecords, err := am.hub.FindAllRecords("alerts",
|
||||
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
|
||||
dbx.NewExp("system={:system} AND name!='Status'", dbx.Params{"system": systemRecord.Id}),
|
||||
)
|
||||
if err != nil || len(alertRecords) == 0 {
|
||||
// log.Println("no alerts found for system")
|
||||
@@ -54,6 +54,15 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
}
|
||||
val = data.Info.DashboardTemp
|
||||
unit = "°C"
|
||||
case "LoadAvg1":
|
||||
val = data.Info.LoadAvg1
|
||||
unit = ""
|
||||
case "LoadAvg5":
|
||||
val = data.Info.LoadAvg5
|
||||
unit = ""
|
||||
case "LoadAvg15":
|
||||
val = data.Info.LoadAvg15
|
||||
unit = ""
|
||||
}
|
||||
|
||||
triggered := alertRecord.GetBool("triggered")
|
||||
@@ -190,6 +199,12 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
}
|
||||
alert.mapSums[key] += temp
|
||||
}
|
||||
case "LoadAvg1":
|
||||
alert.val += stats.LoadAvg1
|
||||
case "LoadAvg5":
|
||||
alert.val += stats.LoadAvg5
|
||||
case "LoadAvg15":
|
||||
alert.val += stats.LoadAvg15
|
||||
default:
|
||||
continue
|
||||
}
|
||||
@@ -247,6 +262,10 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
||||
if alert.name == "Disk" {
|
||||
alert.name += " usage"
|
||||
}
|
||||
// format LoadAvg5 and LoadAvg15
|
||||
if after, ok := strings.CutPrefix(alert.name, "LoadAvg"); ok {
|
||||
alert.name = after + "m Load"
|
||||
}
|
||||
|
||||
// make title alert name lowercase if not CPU
|
||||
titleAlertName := alert.name
|
||||
|
||||
@@ -31,6 +31,9 @@ type Stats struct {
|
||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
||||
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
@@ -89,6 +92,9 @@ type Info struct {
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
Os Os `json:"os" cbor:"14,keyasint"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"beszel/internal/hub/expirymap"
|
||||
"beszel/internal/hub/ws"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
@@ -17,118 +17,96 @@ import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// tokenMap maps tokens to user IDs for universal tokens
|
||||
var tokenMap *expirymap.ExpiryMap[string]
|
||||
|
||||
// agentConnectRequest holds information related to an agent's connection attempt.
|
||||
type agentConnectRequest struct {
|
||||
hub *Hub
|
||||
req *http.Request
|
||||
res http.ResponseWriter
|
||||
token string
|
||||
agentSemVer semver.Version
|
||||
// for universal token
|
||||
// isUniversalToken is true if the token is a universal token.
|
||||
isUniversalToken bool
|
||||
userId string
|
||||
remoteAddr string
|
||||
// userId is the user ID associated with the universal token.
|
||||
userId string
|
||||
}
|
||||
|
||||
// validateAgentHeaders validates the required headers from agent connection requests.
|
||||
func (h *Hub) validateAgentHeaders(headers http.Header) (string, string, error) {
|
||||
token := headers.Get("X-Token")
|
||||
agentVersion := headers.Get("X-Beszel")
|
||||
// universalTokenMap stores active universal tokens and their associated user IDs.
|
||||
var universalTokenMap tokenMap
|
||||
|
||||
if agentVersion == "" || token == "" || len(token) > 512 {
|
||||
return "", "", errors.New("")
|
||||
}
|
||||
return token, agentVersion, nil
|
||||
type tokenMap struct {
|
||||
store *expirymap.ExpiryMap[string]
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// getFingerprintRecord retrieves fingerprint data from the database by token.
|
||||
func (h *Hub) getFingerprintRecord(token string, recordData *ws.FingerprintRecord) error {
|
||||
err := h.DB().NewQuery("SELECT id, system, fingerprint, token FROM fingerprints WHERE token = {:token}").
|
||||
Bind(dbx.Params{
|
||||
"token": token,
|
||||
}).
|
||||
One(recordData)
|
||||
return err
|
||||
// getMap returns the expirymap, creating it if necessary.
|
||||
func (tm *tokenMap) GetMap() *expirymap.ExpiryMap[string] {
|
||||
tm.once.Do(func() {
|
||||
tm.store = expirymap.New[string](time.Hour)
|
||||
})
|
||||
return tm.store
|
||||
}
|
||||
|
||||
// sendResponseError sends an HTTP error response with the given status code and message.
|
||||
func sendResponseError(res http.ResponseWriter, code int, message string) error {
|
||||
res.WriteHeader(code)
|
||||
if message != "" {
|
||||
res.Write([]byte(message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAgentConnect handles the incoming connection request from the agent.
|
||||
// handleAgentConnect is the HTTP handler for an agent's connection request.
|
||||
func (h *Hub) handleAgentConnect(e *core.RequestEvent) error {
|
||||
if err := h.agentConnect(e.Request, e.Response); err != nil {
|
||||
return err
|
||||
}
|
||||
agentRequest := agentConnectRequest{req: e.Request, res: e.Response, hub: h}
|
||||
_ = agentRequest.agentConnect()
|
||||
return nil
|
||||
}
|
||||
|
||||
// agentConnect handles agent connection requests, validating credentials and upgrading to WebSocket.
|
||||
func (h *Hub) agentConnect(req *http.Request, res http.ResponseWriter) (err error) {
|
||||
var agentConnectRequest agentConnectRequest
|
||||
// agentConnect validates agent credentials and upgrades the connection to a WebSocket.
|
||||
func (acr *agentConnectRequest) agentConnect() (err error) {
|
||||
var agentVersion string
|
||||
// check if user agent and token are valid
|
||||
agentConnectRequest.token, agentVersion, err = h.validateAgentHeaders(req.Header)
|
||||
|
||||
acr.token, agentVersion, err = acr.validateAgentHeaders(acr.req.Header)
|
||||
if err != nil {
|
||||
return sendResponseError(res, http.StatusUnauthorized, "")
|
||||
return acr.sendResponseError(acr.res, http.StatusBadRequest, "")
|
||||
}
|
||||
|
||||
// Pull fingerprint from database matching token
|
||||
var fpRecord ws.FingerprintRecord
|
||||
err = h.getFingerprintRecord(agentConnectRequest.token, &fpRecord)
|
||||
// Check if token is an active universal token
|
||||
acr.userId, acr.isUniversalToken = universalTokenMap.GetMap().GetOk(acr.token)
|
||||
|
||||
// if no existing record, check if token is a universal token
|
||||
if err != nil {
|
||||
if err = checkUniversalToken(&agentConnectRequest); err == nil {
|
||||
// if this is a universal token, set the remote address and new record token
|
||||
agentConnectRequest.remoteAddr = getRealIP(req)
|
||||
fpRecord.Token = agentConnectRequest.token
|
||||
}
|
||||
}
|
||||
|
||||
// If no matching token, return unauthorized
|
||||
if err != nil {
|
||||
return sendResponseError(res, http.StatusUnauthorized, "Invalid token")
|
||||
// Find matching fingerprint records for this token
|
||||
fpRecords := getFingerprintRecordsByToken(acr.token, acr.hub)
|
||||
if len(fpRecords) == 0 && !acr.isUniversalToken {
|
||||
// Invalid token - no records found and not a universal token
|
||||
return acr.sendResponseError(acr.res, http.StatusUnauthorized, "Invalid token")
|
||||
}
|
||||
|
||||
// Validate agent version
|
||||
agentConnectRequest.agentSemVer, err = semver.Parse(agentVersion)
|
||||
acr.agentSemVer, err = semver.Parse(agentVersion)
|
||||
if err != nil {
|
||||
return sendResponseError(res, http.StatusUnauthorized, "Invalid agent version")
|
||||
return acr.sendResponseError(acr.res, http.StatusUnauthorized, "Invalid agent version")
|
||||
}
|
||||
|
||||
// Upgrade connection to WebSocket
|
||||
conn, err := ws.GetUpgrader().Upgrade(res, req)
|
||||
conn, err := ws.GetUpgrader().Upgrade(acr.res, acr.req)
|
||||
if err != nil {
|
||||
return sendResponseError(res, http.StatusInternalServerError, "WebSocket upgrade failed")
|
||||
return acr.sendResponseError(acr.res, http.StatusInternalServerError, "WebSocket upgrade failed")
|
||||
}
|
||||
|
||||
go h.verifyWsConn(conn, agentConnectRequest, fpRecord)
|
||||
go acr.verifyWsConn(conn, fpRecords)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyWsConn verifies the WebSocket connection using agent's fingerprint and SSH key signature.
|
||||
func (h *Hub) verifyWsConn(conn *gws.Conn, acr agentConnectRequest, fpRecord ws.FingerprintRecord) (err error) {
|
||||
// verifyWsConn verifies the WebSocket connection using the agent's fingerprint and
|
||||
// SSH key signature, then adds the system to the system manager.
|
||||
func (acr *agentConnectRequest) verifyWsConn(conn *gws.Conn, fpRecords []ws.FingerprintRecord) (err error) {
|
||||
wsConn := ws.NewWsConnection(conn)
|
||||
// must be set before the read loop
|
||||
|
||||
// must set wsConn in connection store before the read loop
|
||||
conn.Session().Store("wsConn", wsConn)
|
||||
|
||||
// make sure connection is closed if there is an error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
wsConn.Close()
|
||||
h.Logger().Error("WebSocket error", "error", err, "system", fpRecord.SystemId)
|
||||
wsConn.Close([]byte(err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
go conn.ReadLoop()
|
||||
|
||||
signer, err := h.GetSSHKey("")
|
||||
signer, err := acr.hub.GetSSHKey("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,40 +116,152 @@ func (h *Hub) verifyWsConn(conn *gws.Conn, acr agentConnectRequest, fpRecord ws.
|
||||
return err
|
||||
}
|
||||
|
||||
// Create system if using universal token
|
||||
if acr.isUniversalToken {
|
||||
if acr.userId == "" {
|
||||
return errors.New("token user not found")
|
||||
}
|
||||
fpRecord.SystemId, err = h.createSystemFromAgentData(&acr, agentFingerprint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create system from universal token: %w", err)
|
||||
}
|
||||
// Find or create the appropriate system for this token and fingerprint
|
||||
fpRecord, err := acr.findOrCreateSystemForToken(fpRecords, agentFingerprint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
// If no current fingerprint, update with new fingerprint (first time connecting)
|
||||
case fpRecord.Fingerprint == "":
|
||||
if err := h.SetFingerprint(&fpRecord, agentFingerprint.Fingerprint); err != nil {
|
||||
return err
|
||||
}
|
||||
// Abort if fingerprint exists but doesn't match (different machine)
|
||||
case fpRecord.Fingerprint != agentFingerprint.Fingerprint:
|
||||
return errors.New("fingerprint mismatch")
|
||||
}
|
||||
|
||||
return h.sm.AddWebSocketSystem(fpRecord.SystemId, acr.agentSemVer, wsConn)
|
||||
return acr.hub.sm.AddWebSocketSystem(fpRecord.SystemId, acr.agentSemVer, wsConn)
|
||||
}
|
||||
|
||||
// createSystemFromAgentData creates a new system record using data from the agent
|
||||
func (h *Hub) createSystemFromAgentData(acr *agentConnectRequest, agentFingerprint common.FingerprintResponse) (recordId string, err error) {
|
||||
systemsCollection, err := h.FindCollectionByNameOrId("systems")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find systems collection: %w", err)
|
||||
// validateAgentHeaders extracts and validates the token and agent version from HTTP headers.
|
||||
func (acr *agentConnectRequest) validateAgentHeaders(headers http.Header) (string, string, error) {
|
||||
token := headers.Get("X-Token")
|
||||
agentVersion := headers.Get("X-Beszel")
|
||||
|
||||
if agentVersion == "" || token == "" || len(token) > 64 {
|
||||
return "", "", errors.New("")
|
||||
}
|
||||
return token, agentVersion, nil
|
||||
}
|
||||
|
||||
// sendResponseError writes an HTTP error response.
|
||||
func (acr *agentConnectRequest) sendResponseError(res http.ResponseWriter, code int, message string) error {
|
||||
res.WriteHeader(code)
|
||||
if message != "" {
|
||||
res.Write([]byte(message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFingerprintRecordsByToken retrieves all fingerprint records associated with a given token.
|
||||
func getFingerprintRecordsByToken(token string, h *Hub) []ws.FingerprintRecord {
|
||||
var records []ws.FingerprintRecord
|
||||
// All will populate empty slice even on error
|
||||
_ = h.DB().NewQuery("SELECT id, system, fingerprint, token FROM fingerprints WHERE token = {:token}").
|
||||
Bind(dbx.Params{
|
||||
"token": token,
|
||||
}).
|
||||
All(&records)
|
||||
return records
|
||||
}
|
||||
|
||||
// findOrCreateSystemForToken finds an existing system matching the token and fingerprint,
|
||||
// or creates a new one for a universal token.
|
||||
func (acr *agentConnectRequest) findOrCreateSystemForToken(fpRecords []ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {
|
||||
// No records - only valid for active universal tokens
|
||||
if len(fpRecords) == 0 {
|
||||
return acr.handleNoRecords(agentFingerprint)
|
||||
}
|
||||
|
||||
// Single record - handle as regular token
|
||||
if len(fpRecords) == 1 && !acr.isUniversalToken {
|
||||
return acr.handleSingleRecord(fpRecords[0], agentFingerprint)
|
||||
}
|
||||
|
||||
// Multiple records or universal token - look for matching fingerprint
|
||||
return acr.handleMultipleRecordsOrUniversalToken(fpRecords, agentFingerprint)
|
||||
}
|
||||
|
||||
// handleNoRecords handles the case where no fingerprint records are found for a token.
|
||||
// A new system is created if the token is a valid universal token.
|
||||
func (acr *agentConnectRequest) handleNoRecords(agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {
|
||||
var fpRecord ws.FingerprintRecord
|
||||
|
||||
if !acr.isUniversalToken || acr.userId == "" {
|
||||
return fpRecord, errors.New("no matching fingerprints")
|
||||
}
|
||||
|
||||
return acr.createNewSystemForUniversalToken(agentFingerprint)
|
||||
}
|
||||
|
||||
// handleSingleRecord handles the case with a single fingerprint record. It validates
|
||||
// the agent's fingerprint against the stored one, or sets it on first connect.
|
||||
func (acr *agentConnectRequest) handleSingleRecord(fpRecord ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {
|
||||
// If no current fingerprint, update with new fingerprint (first time connecting)
|
||||
if fpRecord.Fingerprint == "" {
|
||||
if err := acr.hub.SetFingerprint(&fpRecord, agentFingerprint.Fingerprint); err != nil {
|
||||
return fpRecord, err
|
||||
}
|
||||
// Update the record with the fingerprint that was set
|
||||
fpRecord.Fingerprint = agentFingerprint.Fingerprint
|
||||
return fpRecord, nil
|
||||
}
|
||||
|
||||
// Abort if fingerprint exists but doesn't match (different machine)
|
||||
if fpRecord.Fingerprint != agentFingerprint.Fingerprint {
|
||||
return fpRecord, errors.New("fingerprint mismatch")
|
||||
}
|
||||
|
||||
return fpRecord, nil
|
||||
}
|
||||
|
||||
// handleMultipleRecordsOrUniversalToken finds a matching fingerprint from multiple records.
|
||||
// If no match is found and the token is a universal token, a new system is created.
|
||||
func (acr *agentConnectRequest) handleMultipleRecordsOrUniversalToken(fpRecords []ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {
|
||||
// Return existing record with matching fingerprint if found
|
||||
for i := range fpRecords {
|
||||
if fpRecords[i].Fingerprint == agentFingerprint.Fingerprint {
|
||||
return fpRecords[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
// No matching fingerprint record found, but it's
|
||||
// an active universal token so create a new system
|
||||
if acr.isUniversalToken {
|
||||
return acr.createNewSystemForUniversalToken(agentFingerprint)
|
||||
}
|
||||
|
||||
return ws.FingerprintRecord{}, errors.New("fingerprint mismatch")
|
||||
}
|
||||
|
||||
// createNewSystemForUniversalToken creates a new system and fingerprint record for a universal token.
|
||||
func (acr *agentConnectRequest) createNewSystemForUniversalToken(agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {
|
||||
var fpRecord ws.FingerprintRecord
|
||||
if !acr.isUniversalToken || acr.userId == "" {
|
||||
return fpRecord, errors.New("invalid token")
|
||||
}
|
||||
|
||||
fpRecord.Token = acr.token
|
||||
|
||||
systemId, err := acr.createSystem(agentFingerprint)
|
||||
if err != nil {
|
||||
return fpRecord, err
|
||||
}
|
||||
fpRecord.SystemId = systemId
|
||||
|
||||
// Set the fingerprint for the new system
|
||||
if err := acr.hub.SetFingerprint(&fpRecord, agentFingerprint.Fingerprint); err != nil {
|
||||
return fpRecord, err
|
||||
}
|
||||
|
||||
// Update the record with the fingerprint that was set
|
||||
fpRecord.Fingerprint = agentFingerprint.Fingerprint
|
||||
|
||||
return fpRecord, nil
|
||||
}
|
||||
|
||||
// createSystem creates a new system record in the database using details from the agent.
|
||||
func (acr *agentConnectRequest) createSystem(agentFingerprint common.FingerprintResponse) (recordId string, err error) {
|
||||
systemsCollection, err := acr.hub.FindCachedCollectionByNameOrId("systems")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
remoteAddr := getRealIP(acr.req)
|
||||
// separate port from address
|
||||
if agentFingerprint.Hostname == "" {
|
||||
agentFingerprint.Hostname = acr.remoteAddr
|
||||
agentFingerprint.Hostname = remoteAddr
|
||||
}
|
||||
if agentFingerprint.Port == "" {
|
||||
agentFingerprint.Port = "45876"
|
||||
@@ -179,14 +269,14 @@ func (h *Hub) createSystemFromAgentData(acr *agentConnectRequest, agentFingerpri
|
||||
// create new record
|
||||
systemRecord := core.NewRecord(systemsCollection)
|
||||
systemRecord.Set("name", agentFingerprint.Hostname)
|
||||
systemRecord.Set("host", acr.remoteAddr)
|
||||
systemRecord.Set("host", remoteAddr)
|
||||
systemRecord.Set("port", agentFingerprint.Port)
|
||||
systemRecord.Set("users", []string{acr.userId})
|
||||
|
||||
return systemRecord.Id, h.Save(systemRecord)
|
||||
return systemRecord.Id, acr.hub.Save(systemRecord)
|
||||
}
|
||||
|
||||
// SetFingerprint updates the fingerprint for a given record ID.
|
||||
// SetFingerprint creates or updates a fingerprint record in the database.
|
||||
func (h *Hub) SetFingerprint(fpRecord *ws.FingerprintRecord, fingerprint string) (err error) {
|
||||
// // can't use raw query here because it doesn't trigger SSE
|
||||
var record *core.Record
|
||||
@@ -207,25 +297,8 @@ func (h *Hub) SetFingerprint(fpRecord *ws.FingerprintRecord, fingerprint string)
|
||||
return h.SaveNoValidate(record)
|
||||
}
|
||||
|
||||
func getTokenMap() *expirymap.ExpiryMap[string] {
|
||||
if tokenMap == nil {
|
||||
tokenMap = expirymap.New[string](time.Hour)
|
||||
}
|
||||
return tokenMap
|
||||
}
|
||||
|
||||
func checkUniversalToken(acr *agentConnectRequest) (err error) {
|
||||
if tokenMap == nil {
|
||||
tokenMap = expirymap.New[string](time.Hour)
|
||||
}
|
||||
acr.userId, acr.isUniversalToken = tokenMap.GetOk(acr.token)
|
||||
if !acr.isUniversalToken {
|
||||
return errors.New("invalid token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRealIP attempts to extract the real IP address from the request headers.
|
||||
// getRealIP extracts the client's real IP address from request headers,
|
||||
// checking common proxy headers before falling back to the remote address.
|
||||
func getRealIP(r *http.Request) string {
|
||||
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
|
||||
return ip
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -215,7 +215,7 @@ func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||
|
||||
// registerCronJobs sets up scheduled tasks
|
||||
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
// delete old records once every hour
|
||||
// delete old system_stats and alerts_history records once every hour
|
||||
h.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||
// create longer records every 10 minutes
|
||||
h.Cron().MustAdd("create longer records", "*/10 * * * *", h.rm.CreateLongerRecords)
|
||||
@@ -259,7 +259,7 @@ func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
|
||||
tokenMap := getTokenMap()
|
||||
tokenMap := universalTokenMap.GetMap()
|
||||
userID := info.Auth.Id
|
||||
query := e.Request.URL.Query()
|
||||
token := query.Get("token")
|
||||
|
||||
@@ -254,5 +254,3 @@ func TestGetSSHKey(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create test records
|
||||
|
||||
@@ -365,7 +365,7 @@ func (sys *System) closeSSHConnection() {
|
||||
// The system will be set as down a few seconds later if the connection is not re-established.
|
||||
func (sys *System) closeWebSocketConnection() {
|
||||
if sys.WsConn != nil {
|
||||
sys.WsConn.Close()
|
||||
sys.WsConn.Close(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,8 +159,10 @@ func (sm *SystemManager) onRecordUpdate(e *core.RecordEvent) error {
|
||||
// - down: Triggers status change alerts
|
||||
func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
|
||||
newStatus := e.Record.GetString("status")
|
||||
prevStatus := pending
|
||||
system, ok := sm.systems.GetOk(e.Record.Id)
|
||||
if ok {
|
||||
prevStatus = system.Status
|
||||
system.Status = newStatus
|
||||
}
|
||||
|
||||
@@ -182,6 +184,7 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
|
||||
if err := sm.AddRecord(e.Record, nil); err != nil {
|
||||
e.App.Logger().Error("Error adding record", "err", err)
|
||||
}
|
||||
_ = deactivateAlerts(e.App, e.Record.Id)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
@@ -190,8 +193,6 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
|
||||
return sm.AddRecord(e.Record, nil)
|
||||
}
|
||||
|
||||
prevStatus := system.Status
|
||||
|
||||
// Trigger system alerts when system comes online
|
||||
if newStatus == up {
|
||||
if err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil {
|
||||
|
||||
@@ -77,7 +77,7 @@ func (h *Handler) OnMessage(conn *gws.Conn, message *gws.Message) {
|
||||
case wsConn.(*WsConn).responseChan <- message:
|
||||
default:
|
||||
// close if the connection is not expecting a response
|
||||
wsConn.(*WsConn).Close()
|
||||
wsConn.(*WsConn).Close(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +100,9 @@ func (h *Handler) OnClose(conn *gws.Conn, err error) {
|
||||
}
|
||||
|
||||
// Close terminates the WebSocket connection gracefully.
|
||||
func (ws *WsConn) Close() {
|
||||
func (ws *WsConn) Close(msg []byte) {
|
||||
if ws.IsConnected() {
|
||||
ws.conn.WriteClose(1000, nil)
|
||||
ws.conn.WriteClose(1000, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (ws *WsConn) RequestSystemData(data *system.CombinedData) error {
|
||||
})
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
ws.Close()
|
||||
ws.Close(nil)
|
||||
return gws.ErrConnClosed
|
||||
case message = <-ws.responseChan:
|
||||
}
|
||||
@@ -140,11 +140,12 @@ func (ws *WsConn) RequestSystemData(data *system.CombinedData) error {
|
||||
|
||||
// GetFingerprint authenticates with the agent using SSH signature and returns the agent's fingerprint.
|
||||
func (ws *WsConn) GetFingerprint(token string, signer ssh.Signer, needSysInfo bool) (common.FingerprintResponse, error) {
|
||||
var clientFingerprint common.FingerprintResponse
|
||||
challenge := []byte(token)
|
||||
|
||||
signature, err := signer.Sign(nil, challenge)
|
||||
if err != nil {
|
||||
return common.FingerprintResponse{}, err
|
||||
return clientFingerprint, err
|
||||
}
|
||||
|
||||
err = ws.sendMessage(common.HubRequest[any]{
|
||||
@@ -155,24 +156,19 @@ func (ws *WsConn) GetFingerprint(token string, signer ssh.Signer, needSysInfo bo
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return common.FingerprintResponse{}, err
|
||||
return clientFingerprint, err
|
||||
}
|
||||
|
||||
var message *gws.Message
|
||||
var clientFingerprint common.FingerprintResponse
|
||||
select {
|
||||
case message = <-ws.responseChan:
|
||||
case <-time.After(10 * time.Second):
|
||||
return common.FingerprintResponse{}, errors.New("request expired")
|
||||
return clientFingerprint, errors.New("request expired")
|
||||
}
|
||||
defer message.Close()
|
||||
|
||||
err = cbor.Unmarshal(message.Data.Bytes(), &clientFingerprint)
|
||||
if err != nil {
|
||||
return common.FingerprintResponse{}, err
|
||||
}
|
||||
|
||||
return clientFingerprint, nil
|
||||
return clientFingerprint, err
|
||||
}
|
||||
|
||||
// IsConnected returns true if the WebSocket connection is active.
|
||||
|
||||
@@ -203,6 +203,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.DiskWritePs += stats.DiskWritePs
|
||||
sum.NetworkSent += stats.NetworkSent
|
||||
sum.NetworkRecv += stats.NetworkRecv
|
||||
sum.LoadAvg1 += stats.LoadAvg1
|
||||
sum.LoadAvg5 += stats.LoadAvg5
|
||||
sum.LoadAvg15 += stats.LoadAvg15
|
||||
// Set peak values
|
||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
|
||||
@@ -278,7 +281,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
|
||||
sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
|
||||
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
|
||||
|
||||
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count)
|
||||
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count)
|
||||
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count)
|
||||
// Average temperatures
|
||||
if sum.Temperatures != nil && tempCount > 0 {
|
||||
for key := range sum.Temperatures {
|
||||
@@ -361,12 +366,46 @@ func (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds
|
||||
return result
|
||||
}
|
||||
|
||||
// Deletes records older than what is displayed in the UI
|
||||
// Delete old records
|
||||
func (rm *RecordManager) DeleteOldRecords() {
|
||||
// Define the collections to process
|
||||
rm.app.RunInTransaction(func(txApp core.App) error {
|
||||
err := deleteOldSystemStats(txApp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = deleteOldAlertsHistory(txApp, 200, 250)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Delete old alerts history records
|
||||
func deleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
|
||||
db := app.DB()
|
||||
var users []struct {
|
||||
Id string `db:"user"`
|
||||
}
|
||||
err := db.NewQuery("SELECT user, COUNT(*) as count FROM alerts_history GROUP BY user HAVING count > {:countBeforeDeletion}").Bind(dbx.Params{"countBeforeDeletion": countBeforeDeletion}).All(&users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range users {
|
||||
_, err = db.NewQuery("DELETE FROM alerts_history WHERE user = {:user} AND id NOT IN (SELECT id FROM alerts_history WHERE user = {:user} ORDER BY created DESC LIMIT {:countToKeep})").Bind(dbx.Params{"user": user.Id, "countToKeep": countToKeep}).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deletes system_stats records older than what is displayed in the UI
|
||||
func deleteOldSystemStats(app core.App) error {
|
||||
// Collections to process
|
||||
collections := [2]string{"system_stats", "container_stats"}
|
||||
|
||||
// Define record types and their retention periods
|
||||
// Record types and their retention periods
|
||||
type RecordDeletionData struct {
|
||||
recordType string
|
||||
retention time.Duration
|
||||
@@ -382,10 +421,9 @@ func (rm *RecordManager) DeleteOldRecords() {
|
||||
now := time.Now().UTC()
|
||||
|
||||
for _, collection := range collections {
|
||||
// Build the WHERE clause dynamically
|
||||
// Build the WHERE clause
|
||||
var conditionParts []string
|
||||
var params dbx.Params = make(map[string]any)
|
||||
|
||||
for i := range recordData {
|
||||
rd := recordData[i]
|
||||
// Create parameterized condition for this record type
|
||||
@@ -393,19 +431,15 @@ func (rm *RecordManager) DeleteOldRecords() {
|
||||
conditionParts = append(conditionParts, fmt.Sprintf("(type = '%s' AND created < {:%s})", rd.recordType, dateParam))
|
||||
params[dateParam] = now.Add(-rd.retention)
|
||||
}
|
||||
|
||||
// Combine conditions with OR
|
||||
conditionStr := strings.Join(conditionParts, " OR ")
|
||||
|
||||
// Construct the full raw query
|
||||
// Construct and execute the full raw query
|
||||
rawQuery := fmt.Sprintf("DELETE FROM %s WHERE %s", collection, conditionStr)
|
||||
|
||||
// Execute the query with parameters
|
||||
if _, err := rm.app.DB().NewQuery(rawQuery).Bind(params).Execute(); err != nil {
|
||||
// return fmt.Errorf("failed to delete from %s: %v", collection, err)
|
||||
rm.app.Logger().Error("failed to delete", "collection", collection, "error", err)
|
||||
if _, err := app.DB().NewQuery(rawQuery).Bind(params).Execute(); err != nil {
|
||||
return fmt.Errorf("failed to delete from %s: %v", collection, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* Round float to two decimals */
|
||||
|
||||
381
beszel/internal/records/records_test.go
Normal file
381
beszel/internal/records/records_test.go
Normal file
@@ -0,0 +1,381 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package records_test
|
||||
|
||||
import (
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDeleteOldRecords tests the main DeleteOldRecords function
|
||||
func TestDeleteOldRecords(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
rm := records.NewRecordManager(hub)
|
||||
|
||||
// Create test user for alerts history
|
||||
user, err := tests.CreateUser(hub, "test@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create test system
|
||||
system, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system",
|
||||
"host": "localhost",
|
||||
"port": "45876",
|
||||
"status": "up",
|
||||
"users": []string{user.Id},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Create old system_stats records that should be deleted
|
||||
var record *core.Record
|
||||
record, err = tests.CreateRecord(hub, "system_stats", map[string]any{
|
||||
"system": system.Id,
|
||||
"type": "1m",
|
||||
"stats": `{"cpu": 50.0, "mem": 1024}`,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// created is autodate field, so we need to set it manually
|
||||
record.SetRaw("created", now.UTC().Add(-2*time.Hour).Format(types.DefaultDateLayout))
|
||||
err = hub.SaveNoValidate(record)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, record)
|
||||
require.InDelta(t, record.GetDateTime("created").Time().UTC().Unix(), now.UTC().Add(-2*time.Hour).Unix(), 1)
|
||||
require.Equal(t, record.Get("system"), system.Id)
|
||||
require.Equal(t, record.Get("type"), "1m")
|
||||
|
||||
// Create recent system_stats record that should be kept
|
||||
_, err = tests.CreateRecord(hub, "system_stats", map[string]any{
|
||||
"system": system.Id,
|
||||
"type": "1m",
|
||||
"stats": `{"cpu": 30.0, "mem": 512}`,
|
||||
"created": now.Add(-30 * time.Minute), // 30 minutes old, should be kept
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create many alerts history records to trigger deletion
|
||||
for i := range 260 { // More than countBeforeDeletion (250)
|
||||
_, err = tests.CreateRecord(hub, "alerts_history", map[string]any{
|
||||
"user": user.Id,
|
||||
"name": "CPU",
|
||||
"value": i + 1,
|
||||
"system": system.Id,
|
||||
"created": now.Add(-time.Duration(i) * time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Count records before deletion
|
||||
systemStatsCountBefore, err := hub.CountRecords("system_stats")
|
||||
require.NoError(t, err)
|
||||
alertsCountBefore, err := hub.CountRecords("alerts_history")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run deletion
|
||||
rm.DeleteOldRecords()
|
||||
|
||||
// Count records after deletion
|
||||
systemStatsCountAfter, err := hub.CountRecords("system_stats")
|
||||
require.NoError(t, err)
|
||||
alertsCountAfter, err := hub.CountRecords("alerts_history")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify old system stats were deleted
|
||||
assert.Less(t, systemStatsCountAfter, systemStatsCountBefore, "Old system stats should be deleted")
|
||||
|
||||
// Verify alerts history was trimmed
|
||||
assert.Less(t, alertsCountAfter, alertsCountBefore, "Excessive alerts history should be deleted")
|
||||
assert.Equal(t, alertsCountAfter, int64(200), "Alerts count should be equal to countToKeep (200)")
|
||||
}
|
||||
|
||||
// TestDeleteOldSystemStats tests the deleteOldSystemStats function
|
||||
func TestDeleteOldSystemStats(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
// Create test system
|
||||
user, err := tests.CreateUser(hub, "test@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
system, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system",
|
||||
"host": "localhost",
|
||||
"port": "45876",
|
||||
"status": "up",
|
||||
"users": []string{user.Id},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Test data for different record types and their retention periods
|
||||
testCases := []struct {
|
||||
recordType string
|
||||
retention time.Duration
|
||||
shouldBeKept bool
|
||||
ageFromNow time.Duration
|
||||
description string
|
||||
}{
|
||||
{"1m", time.Hour, true, 30 * time.Minute, "1m record within 1 hour should be kept"},
|
||||
{"1m", time.Hour, false, 2 * time.Hour, "1m record older than 1 hour should be deleted"},
|
||||
{"10m", 12 * time.Hour, true, 6 * time.Hour, "10m record within 12 hours should be kept"},
|
||||
{"10m", 12 * time.Hour, false, 24 * time.Hour, "10m record older than 12 hours should be deleted"},
|
||||
{"20m", 24 * time.Hour, true, 12 * time.Hour, "20m record within 24 hours should be kept"},
|
||||
{"20m", 24 * time.Hour, false, 48 * time.Hour, "20m record older than 24 hours should be deleted"},
|
||||
{"120m", 7 * 24 * time.Hour, true, 3 * 24 * time.Hour, "120m record within 7 days should be kept"},
|
||||
{"120m", 7 * 24 * time.Hour, false, 10 * 24 * time.Hour, "120m record older than 7 days should be deleted"},
|
||||
{"480m", 30 * 24 * time.Hour, true, 15 * 24 * time.Hour, "480m record within 30 days should be kept"},
|
||||
{"480m", 30 * 24 * time.Hour, false, 45 * 24 * time.Hour, "480m record older than 30 days should be deleted"},
|
||||
}
|
||||
|
||||
// Create test records for both system_stats and container_stats
|
||||
collections := []string{"system_stats", "container_stats"}
|
||||
recordIds := make(map[string][]string)
|
||||
|
||||
for _, collection := range collections {
|
||||
recordIds[collection] = make([]string, 0)
|
||||
|
||||
for i, tc := range testCases {
|
||||
recordTime := now.Add(-tc.ageFromNow)
|
||||
|
||||
var stats string
|
||||
if collection == "system_stats" {
|
||||
stats = fmt.Sprintf(`{"cpu": %d.0, "mem": %d}`, i*10, i*100)
|
||||
} else {
|
||||
stats = fmt.Sprintf(`[{"name": "container%d", "cpu": %d.0, "mem": %d}]`, i, i*5, i*50)
|
||||
}
|
||||
|
||||
record, err := tests.CreateRecord(hub, collection, map[string]any{
|
||||
"system": system.Id,
|
||||
"type": tc.recordType,
|
||||
"stats": stats,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
record.SetRaw("created", recordTime.Format(types.DefaultDateLayout))
|
||||
err = hub.SaveNoValidate(record)
|
||||
require.NoError(t, err)
|
||||
recordIds[collection] = append(recordIds[collection], record.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Run deletion
|
||||
err = records.TestDeleteOldSystemStats(hub)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
for _, collection := range collections {
|
||||
for i, tc := range testCases {
|
||||
recordId := recordIds[collection][i]
|
||||
|
||||
// Try to find the record
|
||||
_, err := hub.FindRecordById(collection, recordId)
|
||||
|
||||
if tc.shouldBeKept {
|
||||
assert.NoError(t, err, "Record should exist: %s", tc.description)
|
||||
} else {
|
||||
assert.Error(t, err, "Record should be deleted: %s", tc.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOldAlertsHistory tests the deleteOldAlertsHistory function
|
||||
func TestDeleteOldAlertsHistory(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
// Create test users
|
||||
user1, err := tests.CreateUser(hub, "user1@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
user2, err := tests.CreateUser(hub, "user2@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
system, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system",
|
||||
"host": "localhost",
|
||||
"port": "45876",
|
||||
"status": "up",
|
||||
"users": []string{user1.Id, user2.Id},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
now := time.Now().UTC()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *core.Record
|
||||
alertCount int
|
||||
countToKeep int
|
||||
countBeforeDeletion int
|
||||
expectedAfterDeletion int
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "User with few alerts (below threshold)",
|
||||
user: user1,
|
||||
alertCount: 100,
|
||||
countToKeep: 50,
|
||||
countBeforeDeletion: 150,
|
||||
expectedAfterDeletion: 100, // No deletion because below threshold
|
||||
description: "User with alerts below countBeforeDeletion should not have any deleted",
|
||||
},
|
||||
{
|
||||
name: "User with many alerts (above threshold)",
|
||||
user: user2,
|
||||
alertCount: 300,
|
||||
countToKeep: 100,
|
||||
countBeforeDeletion: 200,
|
||||
expectedAfterDeletion: 100, // Should be trimmed to countToKeep
|
||||
description: "User with alerts above countBeforeDeletion should be trimmed to countToKeep",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create alerts for this user
|
||||
for i := 0; i < tc.alertCount; i++ {
|
||||
_, err := tests.CreateRecord(hub, "alerts_history", map[string]any{
|
||||
"user": tc.user.Id,
|
||||
"name": "CPU",
|
||||
"value": i + 1,
|
||||
"system": system.Id,
|
||||
"created": now.Add(-time.Duration(i) * time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Count before deletion
|
||||
countBefore, err := hub.CountRecords("alerts_history",
|
||||
dbx.NewExp("user = {:user}", dbx.Params{"user": tc.user.Id}))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(tc.alertCount), countBefore, "Initial count should match")
|
||||
|
||||
// Run deletion
|
||||
err = records.TestDeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count after deletion
|
||||
countAfter, err := hub.CountRecords("alerts_history",
|
||||
dbx.NewExp("user = {:user}", dbx.Params{"user": tc.user.Id}))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(tc.expectedAfterDeletion), countAfter, tc.description)
|
||||
|
||||
// If deletion occurred, verify the most recent records were kept
|
||||
if tc.expectedAfterDeletion < tc.alertCount {
|
||||
records, err := hub.FindRecordsByFilter("alerts_history",
|
||||
"user = {:user}",
|
||||
"-created", // Order by created DESC
|
||||
tc.countToKeep,
|
||||
0,
|
||||
map[string]any{"user": tc.user.Id})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, records, tc.expectedAfterDeletion, "Should have exactly countToKeep records")
|
||||
|
||||
// Verify records are in descending order by created time
|
||||
for i := 1; i < len(records); i++ {
|
||||
prev := records[i-1].GetDateTime("created").Time()
|
||||
curr := records[i].GetDateTime("created").Time()
|
||||
assert.True(t, prev.After(curr) || prev.Equal(curr),
|
||||
"Records should be ordered by created time (newest first)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOldAlertsHistoryEdgeCases tests edge cases for alerts history deletion
|
||||
func TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
t.Run("No users with excessive alerts", func(t *testing.T) {
|
||||
// Create user with few alerts
|
||||
user, err := tests.CreateUser(hub, "few@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
system, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "test-system",
|
||||
"host": "localhost",
|
||||
"port": "45876",
|
||||
"status": "up",
|
||||
"users": []string{user.Id},
|
||||
})
|
||||
|
||||
// Create only 5 alerts (well below threshold)
|
||||
for i := range 5 {
|
||||
_, err := tests.CreateRecord(hub, "alerts_history", map[string]any{
|
||||
"user": user.Id,
|
||||
"name": "CPU",
|
||||
"value": i + 1,
|
||||
"system": system.Id,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Should not error and should not delete anything
|
||||
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := hub.CountRecords("alerts_history")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(5), count, "All alerts should remain")
|
||||
})
|
||||
|
||||
t.Run("Empty alerts_history table", func(t *testing.T) {
|
||||
// Clear any existing alerts
|
||||
_, err := hub.DB().NewQuery("DELETE FROM alerts_history").Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not error with empty table
|
||||
err = records.TestDeleteOldAlertsHistory(hub, 10, 20)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRecordManagerCreation tests RecordManager creation
|
||||
func TestRecordManagerCreation(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
rm := records.NewRecordManager(hub)
|
||||
assert.NotNil(t, rm, "RecordManager should not be nil")
|
||||
}
|
||||
|
||||
// TestTwoDecimals tests the twoDecimals helper function
|
||||
func TestTwoDecimals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input float64
|
||||
expected float64
|
||||
}{
|
||||
{1.234567, 1.23},
|
||||
{1.235, 1.24}, // Should round up
|
||||
{1.0, 1.0},
|
||||
{0.0, 0.0},
|
||||
{-1.234567, -1.23},
|
||||
{-1.235, -1.23}, // Negative rounding
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := records.TestTwoDecimals(tc.input)
|
||||
assert.InDelta(t, tc.expected, result, 0.02, "twoDecimals(%f) should equal %f", tc.input, tc.expected)
|
||||
}
|
||||
}
|
||||
23
beszel/internal/records/records_test_helpers.go
Normal file
23
beszel/internal/records/records_test_helpers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package records
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
// TestDeleteOldSystemStats exposes deleteOldSystemStats for testing
|
||||
func TestDeleteOldSystemStats(app core.App) error {
|
||||
return deleteOldSystemStats(app)
|
||||
}
|
||||
|
||||
// TestDeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing
|
||||
func TestDeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {
|
||||
return deleteOldAlertsHistory(app, countToKeep, countBeforeDeletion)
|
||||
}
|
||||
|
||||
// TestTwoDecimals exposes twoDecimals for testing
|
||||
func TestTwoDecimals(value float64) float64 {
|
||||
return twoDecimals(value)
|
||||
}
|
||||
@@ -17,7 +17,12 @@ type UserSettings struct {
|
||||
ChartTime string `json:"chartTime"`
|
||||
NotificationEmails []string `json:"emails"`
|
||||
NotificationWebhooks []string `json:"webhooks"`
|
||||
// Language string `json:"lang"`
|
||||
// UnitTemp uint8 `json:"unitTemp"` // 0 for Celsius, 1 for Fahrenheit
|
||||
// UnitNet uint8 `json:"unitNet"` // 0 for bytes, 1 for bits
|
||||
// UnitDisk uint8 `json:"unitDisk"` // 0 for bytes, 1 for bits
|
||||
|
||||
// New field for alert history retention (e.g., "1m", "3m", "6m", "1y")
|
||||
AlertHistoryRetention string `json:"alertHistoryRetention,omitempty"`
|
||||
}
|
||||
|
||||
func NewUserManager(app core.App) *UserManager {
|
||||
@@ -39,7 +44,6 @@ func (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {
|
||||
record := e.Record
|
||||
// intialize settings with defaults
|
||||
settings := UserSettings{
|
||||
// Language: "en",
|
||||
ChartTime: "1h",
|
||||
NotificationEmails: []string{},
|
||||
NotificationWebhooks: []string{},
|
||||
|
||||
@@ -75,7 +75,10 @@ func init() {
|
||||
"Memory",
|
||||
"Disk",
|
||||
"Temperature",
|
||||
"Bandwidth"
|
||||
"Bandwidth",
|
||||
"LoadAvg1",
|
||||
"LoadAvg5",
|
||||
"LoadAvg15"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -137,6 +140,124 @@ func init() {
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "pbc_1697146157",
|
||||
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"viewRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id",
|
||||
"name": "alerts_history",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "_pb_users_auth_",
|
||||
"hidden": false,
|
||||
"id": "relation2375276105",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "user",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "2hz5ncl8tizk5nx",
|
||||
"hidden": false,
|
||||
"id": "relation3377271179",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "system",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text2466471794",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "alert_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1579384326",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number494360628",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "value",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "date2276568630",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "resolved",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX ` + "`" + `idx_YdGnup5aqB` + "`" + ` ON ` + "`" + `alerts_history` + "`" + ` (` + "`" + `user` + "`" + `)",
|
||||
"CREATE INDEX ` + "`" + `idx_taLet9VdME` + "`" + ` ON ` + "`" + `alerts_history` + "`" + ` (` + "`" + `created` + "`" + `)"
|
||||
],
|
||||
"system": false
|
||||
},
|
||||
{
|
||||
"id": "juohu4jipgc13v7",
|
||||
"listRule": "@request.auth.id != \"\"",
|
||||
@@ -754,7 +875,6 @@ func init() {
|
||||
LEFT JOIN fingerprints f ON s.id = f.system
|
||||
WHERE f.system IS NULL
|
||||
`).Column(&systemIds)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
TempAdminEmail = "_@b.b"
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
3518
beszel/site/package-lock.json
generated
3518
beszel/site/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.12.0-beta1",
|
||||
"version": "0.12.0-beta2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -51,6 +51,7 @@
|
||||
"@lingui/cli": "^5.3.2",
|
||||
"@lingui/swc-plugin": "^5.5.2",
|
||||
"@lingui/vite-plugin": "^5.3.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@types/bun": "^1.2.15",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
|
||||
164
beszel/site/src/components/alerts-history-columns.tsx
Normal file
164
beszel/site/src/components/alerts-history-columns.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { AlertsHistoryRecord } from "@/types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { alertInfo, formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
|
||||
{
|
||||
accessorKey: "system",
|
||||
enableSorting: true,
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>System</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
|
||||
filterFn: (row, _, filterValue) => {
|
||||
const display = row.original.expand?.system?.name || row.original.system || ""
|
||||
return display.toLowerCase().includes(filterValue.toLowerCase())
|
||||
},
|
||||
},
|
||||
{
|
||||
// accessorKey: "name",
|
||||
id: "name",
|
||||
accessorFn: (record) => {
|
||||
const name = record.name
|
||||
const info = alertInfo[name]
|
||||
return info?.name().replace("cpu", "CPU") || name
|
||||
},
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>Name</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ getValue, row }) => {
|
||||
let name = getValue() as string
|
||||
const info = alertInfo[row.original.name]
|
||||
const Icon = info?.icon
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2 ps-1 min-w-40">
|
||||
{Icon && <Icon className="size-3.5" />}
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
enableSorting: false,
|
||||
header: () => (
|
||||
<Button variant="ghost">
|
||||
<Trans>Value</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell({ row, getValue }) {
|
||||
const name = row.original.name
|
||||
if (name === "Status") {
|
||||
return <span className="ps-2">{t`Down`}</span>
|
||||
}
|
||||
const value = getValue() as number
|
||||
const unit = alertInfo[name]?.unit
|
||||
return (
|
||||
<span className="tabular-nums ps-2.5">
|
||||
{toFixedFloat(value, value < 10 ? 2 : 1)}
|
||||
{unit}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "state",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => (rowA.original.resolved ? 1 : 0) - (rowB.original.resolved ? 1 : 0),
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>State</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resolved = row.original.resolved
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"capitalize pointer-events-none",
|
||||
resolved
|
||||
? "bg-green-100 text-green-800 border-green-200 dark:opacity-80"
|
||||
: "bg-yellow-100 text-yellow-800 border-yellow-200"
|
||||
)}
|
||||
>
|
||||
{/* {resolved ? <CircleCheckIcon className="size-3 me-0.5" /> : <CircleAlertIcon className="size-3 me-0.5" />} */}
|
||||
<Trans>{resolved ? "Resolved" : "Active"}</Trans>
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created",
|
||||
accessorFn: (record) => formatShortDate(record.created),
|
||||
enableSorting: true,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>Created</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ getValue, row }) => (
|
||||
<span className="ps-1 tabular-nums tracking-tight" title={`${row.original.created} UTC`}>
|
||||
{getValue() as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "resolved",
|
||||
enableSorting: true,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>Resolved</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row, getValue }) => {
|
||||
const resolved = getValue() as string | null
|
||||
if (!resolved) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<span className="ps-1 tabular-nums tracking-tight" title={`${row.original.resolved} UTC`}>
|
||||
{formatShortDate(resolved)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "duration",
|
||||
invertSorting: true,
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const aCreated = new Date(rowA.original.created)
|
||||
const bCreated = new Date(rowB.original.created)
|
||||
const aResolved = rowA.original.resolved ? new Date(rowA.original.resolved) : null
|
||||
const bResolved = rowB.original.resolved ? new Date(rowB.original.resolved) : null
|
||||
const aDuration = aResolved ? aResolved.getTime() - aCreated.getTime() : null
|
||||
const bDuration = bResolved ? bResolved.getTime() - bCreated.getTime() : null
|
||||
if (!aDuration && bDuration) return -1
|
||||
if (aDuration && !bDuration) return 1
|
||||
return (aDuration || 0) - (bDuration || 0)
|
||||
},
|
||||
header: ({ column }) => (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
<Trans>Duration</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const duration = formatDuration(row.original.created, row.original.resolved)
|
||||
if (!duration) {
|
||||
return null
|
||||
}
|
||||
return <span className="ps-2">{duration}</span>
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -39,13 +39,33 @@ export default memo(function AlertsButton({ system }: { system: SystemRecord })
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
||||
<DialogContent className="max-h-full sm:max-h-[95svh] overflow-auto max-w-[37rem]">
|
||||
{opened && <AlertDialogContent system={system} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
[opened, hasAlert]
|
||||
)
|
||||
|
||||
// return useMemo(
|
||||
// () => (
|
||||
// <Sheet>
|
||||
// <SheetTrigger asChild>
|
||||
// <Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||
// <BellIcon
|
||||
// className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
// "fill-primary": hasAlert,
|
||||
// })}
|
||||
// />
|
||||
// </Button>
|
||||
// </SheetTrigger>
|
||||
// <SheetContent className="max-h-full overflow-auto w-[35em] p-4 sm:p-5">
|
||||
// {opened && <AlertDialogContent system={system} />}
|
||||
// </SheetContent>
|
||||
// </Sheet>
|
||||
// ),
|
||||
// [opened, hasAlert]
|
||||
// )
|
||||
})
|
||||
|
||||
function AlertDialogContent({ system }: { system: SystemRecord }) {
|
||||
|
||||
@@ -217,7 +217,7 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
|
||||
const [checked, setChecked] = useState(data.checked || false)
|
||||
const [min, setMin] = useState(data.min || 10)
|
||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
|
||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : data.alert.start ?? 80))
|
||||
|
||||
const Icon = alertInfo[name].icon
|
||||
|
||||
@@ -268,7 +268,8 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
onValueChange={(val) => {
|
||||
setValue(val[0])
|
||||
}}
|
||||
min={1}
|
||||
step={data.alert.step ?? 1}
|
||||
min={data.alert.min ?? 1}
|
||||
max={alertInfo[name].max ?? 99}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,7 @@ import { t } from "@lingui/core/macro"
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
@@ -30,7 +23,6 @@ const getNestedValue = (path: string, max = false, data: any): number | null =>
|
||||
|
||||
export default memo(function AreaChartDefault({
|
||||
maxToggled = false,
|
||||
unit = " MB/s",
|
||||
chartName,
|
||||
chartData,
|
||||
max,
|
||||
@@ -38,12 +30,11 @@ export default memo(function AreaChartDefault({
|
||||
contentFormatter,
|
||||
}: {
|
||||
maxToggled?: boolean
|
||||
unit?: string
|
||||
chartName: string
|
||||
chartData: ChartData
|
||||
max?: number
|
||||
tickFormatter?: (value: number) => string
|
||||
contentFormatter?: (value: number) => string
|
||||
tickFormatter: (value: number) => string
|
||||
contentFormatter: ({ value }: { value: number }) => string
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const { i18n } = useLingui()
|
||||
@@ -98,15 +89,7 @@ export default memo(function AreaChartDefault({
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={[0, max ?? "auto"]}
|
||||
tickFormatter={(value) => {
|
||||
let val: string
|
||||
if (tickFormatter) {
|
||||
val = tickFormatter(value)
|
||||
} else {
|
||||
val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||
}
|
||||
return updateYAxisWidth(val)
|
||||
}}
|
||||
tickFormatter={(value) => updateYAxisWidth(tickFormatter(value))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
@@ -117,12 +100,7 @@ export default memo(function AreaChartDefault({
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={({ value }) => {
|
||||
if (contentFormatter) {
|
||||
return contentFormatter(value)
|
||||
}
|
||||
return decimalString(value) + unit
|
||||
}}
|
||||
contentFormatter={contentFormatter}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { memo, useMemo } from "react"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
toFixedFloat,
|
||||
getSizeAndUnit,
|
||||
toFixedWithoutTrailingZeros,
|
||||
} from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $containerFilter } from "@/lib/stores"
|
||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { ChartData } from "@/types"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { ChartType } from "@/lib/enums"
|
||||
import { ChartType, Unit } from "@/lib/enums"
|
||||
|
||||
export default memo(function ContainerChart({
|
||||
dataKey,
|
||||
@@ -30,6 +21,7 @@ export default memo(function ContainerChart({
|
||||
unit?: string
|
||||
}) {
|
||||
const filter = useStore($containerFilter)
|
||||
const userSettings = useStore($userSettings)
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
const { containerData } = chartData
|
||||
@@ -84,13 +76,14 @@ export default memo(function ContainerChart({
|
||||
// tick formatter
|
||||
if (chartType === ChartType.CPU) {
|
||||
obj.tickFormatter = (value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2) + unit
|
||||
const val = toFixedFloat(value, 2) + unit
|
||||
return updateYAxisWidth(val)
|
||||
}
|
||||
} else {
|
||||
obj.tickFormatter = (value) => {
|
||||
const { v, u } = getSizeAndUnit(value, false)
|
||||
return updateYAxisWidth(`${toFixedFloat(v, 2)}${u}${isNetChart ? "/s" : ""}`)
|
||||
const chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes
|
||||
obj.tickFormatter = (val) => {
|
||||
const { value, unit } = formatBytes(val, isNetChart, chartUnit, true)
|
||||
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||
}
|
||||
}
|
||||
// tooltip formatter
|
||||
@@ -99,12 +92,14 @@ export default memo(function ContainerChart({
|
||||
try {
|
||||
const sent = item?.payload?.[key]?.ns ?? 0
|
||||
const received = item?.payload?.[key]?.nr ?? 0
|
||||
const { value: receivedValue, unit: receivedUnit } = formatBytes(received, true, userSettings.unitNet, true)
|
||||
const { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, true)
|
||||
return (
|
||||
<span className="flex">
|
||||
{decimalString(received)} MB/s
|
||||
{decimalString(receivedValue)} {receivedUnit}
|
||||
<span className="opacity-70 ms-0.5"> rx </span>
|
||||
<Separator orientation="vertical" className="h-3 mx-1.5 bg-primary/40" />
|
||||
{decimalString(sent)} MB/s
|
||||
{decimalString(sentValue)} {sentUnit}
|
||||
<span className="opacity-70 ms-0.5"> tx</span>
|
||||
</span>
|
||||
)
|
||||
@@ -114,8 +109,8 @@ export default memo(function ContainerChart({
|
||||
}
|
||||
} else if (chartType === ChartType.Memory) {
|
||||
obj.toolTipFormatter = (item: any) => {
|
||||
const { v, u } = getSizeAndUnit(item.value, false)
|
||||
return decimalString(v, 2) + u
|
||||
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
||||
return decimalString(value) + " " + unit
|
||||
}
|
||||
} else {
|
||||
obj.toolTipFormatter = (item: any) => decimalString(item.value) + unit
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
decimalString,
|
||||
toFixedFloat,
|
||||
chartMargin,
|
||||
getSizeAndUnit,
|
||||
} from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Unit } from "@/lib/enums"
|
||||
|
||||
export default memo(function DiskChart({
|
||||
dataKey,
|
||||
@@ -53,9 +46,9 @@ export default memo(function DiskChart({
|
||||
minTickGap={6}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
const { v, u } = getSizeAndUnit(value)
|
||||
return updateYAxisWidth(toFixedFloat(v, 2) + u)
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)
|
||||
return updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit)
|
||||
}}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
@@ -66,8 +59,8 @@ export default memo(function DiskChart({
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={({ value }) => {
|
||||
const { v, u } = getSizeAndUnit(value)
|
||||
return decimalString(v) + u
|
||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
||||
return decimalString(convertedValue) + " " + unit
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -8,14 +8,7 @@ import {
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
|
||||
@@ -72,7 +65,7 @@ export default memo(function GpuPowerChart({ chartData }: { chartData: ChartData
|
||||
domain={[0, "auto"]}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2)
|
||||
const val = toFixedFloat(value, 2)
|
||||
return updateYAxisWidth(val + "W")
|
||||
}}
|
||||
tickLine={false}
|
||||
|
||||
91
beszel/site/src/components/charts/load-average-chart.tsx
Normal file
91
beszel/site/src/components/charts/load-average-chart.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { CartesianGrid, Line, LineChart, YAxis } from "recharts"
|
||||
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
xAxis,
|
||||
} from "@/components/ui/chart"
|
||||
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const keys = {
|
||||
l1: {
|
||||
color: "hsl(271, 81%, 60%)", // Purple
|
||||
label: t`1 min`,
|
||||
},
|
||||
l5: {
|
||||
color: "hsl(217, 91%, 60%)", // Blue
|
||||
label: t`5 min`,
|
||||
},
|
||||
l15: {
|
||||
color: "hsl(25, 95%, 53%)", // Orange
|
||||
label: t`15 min`,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChartContainer
|
||||
className={cn("h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity", {
|
||||
"opacity-100": yAxisWidth,
|
||||
})}
|
||||
>
|
||||
<LineChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
domain={[0, "auto"]}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => {
|
||||
return updateYAxisWidth(String(toFixedFloat(value, 2)))
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
animationEasing="ease-out"
|
||||
animationDuration={150}
|
||||
// @ts-ignore
|
||||
// itemSorter={(a, b) => b.value - a.value}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{Object.entries(keys).map(([key, value]: [string, { color: string; label: string }]) => {
|
||||
return (
|
||||
<Line
|
||||
key={key}
|
||||
dataKey={`stats.${key}`}
|
||||
name={value.label}
|
||||
type="monotoneX"
|
||||
dot={false}
|
||||
strokeWidth={1.5}
|
||||
stroke={value.color}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { useYAxisWidth, cn, toFixedFloat, decimalString, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, decimalString, formatShortDate, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { memo } from "react"
|
||||
import { ChartData } from "@/types"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import { Unit } from "@/lib/enums"
|
||||
|
||||
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
@@ -39,8 +40,8 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
const val = toFixedFloat(value, 1)
|
||||
return updateYAxisWidth(val + " GB")
|
||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
||||
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -54,8 +55,11 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
||||
// @ts-ignore
|
||||
itemSorter={(a, b) => a.order - b.order}
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
||||
// indicator="line"
|
||||
contentFormatter={({ value }) => {
|
||||
// mem values are supplied as GB
|
||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
chartMargin,
|
||||
} from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, decimalString, chartMargin, formatBytes, toFixedFloat } from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo } from "react"
|
||||
import { $userSettings } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
|
||||
export default memo(function SwapChart({ chartData }: { chartData: ChartData }) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
const userSettings = useStore($userSettings)
|
||||
|
||||
if (chartData.systemStats.length === 0) {
|
||||
return null
|
||||
@@ -33,11 +29,14 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
||||
direction="ltr"
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
domain={[0, () => toFixedWithoutTrailingZeros(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
|
||||
domain={[0, () => toFixedFloat(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}
|
||||
width={yAxisWidth}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => updateYAxisWidth(value + " GB")}
|
||||
tickFormatter={(value) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)
|
||||
return updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + " " + unit)
|
||||
}}
|
||||
/>
|
||||
{xAxis(chartData)}
|
||||
<ChartTooltip
|
||||
@@ -46,7 +45,11 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + " GB"}
|
||||
contentFormatter={({ value }) => {
|
||||
// mem values are supplied as GB
|
||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
// indicator="line"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -12,17 +12,19 @@ import {
|
||||
useYAxisWidth,
|
||||
cn,
|
||||
formatShortDate,
|
||||
toFixedWithoutTrailingZeros,
|
||||
decimalString,
|
||||
toFixedFloat,
|
||||
chartMargin,
|
||||
formatTemperature,
|
||||
decimalString,
|
||||
} from "@/lib/utils"
|
||||
import { ChartData } from "@/types"
|
||||
import { memo, useMemo } from "react"
|
||||
import { $temperatureFilter } from "@/lib/stores"
|
||||
import { $temperatureFilter, $userSettings } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
|
||||
export default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {
|
||||
const filter = useStore($temperatureFilter)
|
||||
const userSettings = useStore($userSettings)
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
|
||||
if (chartData.systemStats.length === 0) {
|
||||
@@ -72,9 +74,9 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
className="tracking-tighter"
|
||||
domain={[0, "auto"]}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) => {
|
||||
const val = toFixedWithoutTrailingZeros(value, 2)
|
||||
return updateYAxisWidth(val + " °C")
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
|
||||
return updateYAxisWidth(toFixedFloat(value, 2) + " " + unit)
|
||||
}}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -88,7 +90,10 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||
contentFormatter={(item) => decimalString(item.value) + " °C"}
|
||||
contentFormatter={(item) => {
|
||||
const { value, unit } = formatTemperature(item.value, userSettings.unitTemp)
|
||||
return decimalString(value) + " " + unit
|
||||
}}
|
||||
filter={filter}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Server className="me-2 h-4 w-4" />
|
||||
<Server className="me-2 size-4" />
|
||||
<span>{system.name}</span>
|
||||
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
|
||||
</CommandItem>
|
||||
@@ -86,7 +86,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<LayoutDashboard className="me-2 h-4 w-4" />
|
||||
<LayoutDashboard className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Dashboard</Trans>
|
||||
</span>
|
||||
@@ -100,7 +100,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<SettingsIcon className="me-2 h-4 w-4" />
|
||||
<SettingsIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Settings</Trans>
|
||||
</span>
|
||||
@@ -113,7 +113,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<MailIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Notifications</Trans>
|
||||
</span>
|
||||
@@ -125,19 +125,31 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<FingerprintIcon className="me-2 h-4 w-4" />
|
||||
<FingerprintIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Tokens & Fingerprints</Trans>
|
||||
</span>
|
||||
{SettingsShortcut}
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate(getPagePath($router, "settings", { name: "alert-history" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<LogsIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Alert History</Trans>
|
||||
</span>
|
||||
{SettingsShortcut}
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
keywords={["help", "oauth", "oidc"]}
|
||||
onSelect={() => {
|
||||
window.location.href = "https://beszel.dev/guide/what-is-beszel"
|
||||
}}
|
||||
>
|
||||
<BookIcon className="me-2 h-4 w-4" />
|
||||
<BookIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Documentation</Trans>
|
||||
</span>
|
||||
@@ -155,7 +167,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
window.open(prependBasePath("/_/"), "_blank")
|
||||
}}
|
||||
>
|
||||
<UsersIcon className="me-2 h-4 w-4" />
|
||||
<UsersIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Users</Trans>
|
||||
</span>
|
||||
@@ -167,7 +179,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
window.open(prependBasePath("/_/#/logs"), "_blank")
|
||||
}}
|
||||
>
|
||||
<LogsIcon className="me-2 h-4 w-4" />
|
||||
<LogsIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Logs</Trans>
|
||||
</span>
|
||||
@@ -179,7 +191,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
window.open(prependBasePath("/_/#/settings/backups"), "_blank")
|
||||
}}
|
||||
>
|
||||
<DatabaseBackupIcon className="me-2 h-4 w-4" />
|
||||
<DatabaseBackupIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>Backups</Trans>
|
||||
</span>
|
||||
@@ -192,7 +204,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
|
||||
window.open(prependBasePath("/_/#/settings/mail"), "_blank")
|
||||
}}
|
||||
>
|
||||
<MailIcon className="me-2 h-4 w-4" />
|
||||
<MailIcon className="me-2 size-4" />
|
||||
<span>
|
||||
<Trans>SMTP settings</Trans>
|
||||
</span>
|
||||
|
||||
@@ -110,10 +110,14 @@ const ActiveAlerts = memo(({ activeAlerts }: { activeAlerts: AlertRecord[] }) =>
|
||||
{alert.sysname} {info.name().toLowerCase().replace("cpu", "CPU")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
{alert.name === "Status" ? (
|
||||
<Trans>Connection is down</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Exceeds {alert.value}
|
||||
{info.unit} in last <Plural value={alert.min} one="# minute" other="# minutes" />
|
||||
</Trans>
|
||||
)}
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={getPagePath($router, "system", { name: alert.sysname! })}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
import { pb } from "@/lib/stores"
|
||||
import { alertInfo, cn, formatDuration, formatShortDate } from "@/lib/utils"
|
||||
import { AlertsHistoryRecord } from "@/types"
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
useReactTable,
|
||||
flexRender,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { alertsHistoryColumns } from "../../alerts-history-columns"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
DownloadIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
const SectionIntro = memo(() => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-xl font-medium mb-2">
|
||||
<Trans>Alert History</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>View your 200 most recent alerts.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default function AlertsHistoryDataTable() {
|
||||
const [data, setData] = useState<AlertsHistoryRecord[]>([])
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
const { toast } = useToast()
|
||||
const [deleteOpen, setDeleteDialogOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined
|
||||
const pbOptions = {
|
||||
expand: "system",
|
||||
fields: "id,name,value,state,created,resolved,expand.system.name",
|
||||
}
|
||||
// Initial load
|
||||
pb.collection<AlertsHistoryRecord>("alerts_history")
|
||||
.getFullList({
|
||||
...pbOptions,
|
||||
sort: "-created",
|
||||
})
|
||||
.then((records) => setData(records))
|
||||
|
||||
// Subscribe to changes
|
||||
;(async () => {
|
||||
unsubscribe = await pb.collection("alerts_history").subscribe(
|
||||
"*",
|
||||
(e) => {
|
||||
if (e.action === "create") {
|
||||
setData((current) => [e.record as AlertsHistoryRecord, ...current])
|
||||
}
|
||||
if (e.action === "update") {
|
||||
setData((current) => current.map((r) => (r.id === e.record.id ? (e.record as AlertsHistoryRecord) : r)))
|
||||
}
|
||||
if (e.action === "delete") {
|
||||
setData((current) => current.filter((r) => r.id !== e.record.id))
|
||||
}
|
||||
},
|
||||
pbOptions
|
||||
)
|
||||
})()
|
||||
// Unsubscribe on unmount
|
||||
return () => unsubscribe?.()
|
||||
}, [])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
className="ms-2"
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
...alertsHistoryColumns,
|
||||
],
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
globalFilter,
|
||||
},
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
globalFilterFn: (row, _columnId, filterValue) => {
|
||||
const system = row.original.expand?.system?.name ?? ""
|
||||
const name = row.getValue("name") ?? ""
|
||||
const created = row.getValue("created") ?? ""
|
||||
const search = String(filterValue).toLowerCase()
|
||||
return (
|
||||
system.toLowerCase().includes(search) ||
|
||||
(name as string).toLowerCase().includes(search) ||
|
||||
(created as string).toLowerCase().includes(search)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Bulk delete handler
|
||||
const handleBulkDelete = async () => {
|
||||
setDeleteDialogOpen(false)
|
||||
const selectedIds = table.getSelectedRowModel().rows.map((row) => row.original.id)
|
||||
try {
|
||||
let batch = pb.createBatch()
|
||||
let inBatch = 0
|
||||
for (const id of selectedIds) {
|
||||
batch.collection("alerts_history").delete(id)
|
||||
inBatch++
|
||||
if (inBatch > 20) {
|
||||
await batch.send()
|
||||
batch = pb.createBatch()
|
||||
inBatch = 0
|
||||
}
|
||||
}
|
||||
inBatch && (await batch.send())
|
||||
table.resetRowSelection()
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t`Error`,
|
||||
description: `Failed to delete records.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Export to CSV handler
|
||||
const handleExportCSV = () => {
|
||||
const selectedRows = table.getSelectedRowModel().rows
|
||||
if (!selectedRows.length) return
|
||||
const cells: Record<string, (record: AlertsHistoryRecord) => string> = {
|
||||
system: (record) => record.expand?.system?.name || record.system,
|
||||
name: (record) => alertInfo[record.name]?.name() || record.name,
|
||||
value: (record) => record.value + (alertInfo[record.name]?.unit ?? ""),
|
||||
state: (record) => (record.resolved ? t`Resolved` : t`Active`),
|
||||
created: (record) => formatShortDate(record.created),
|
||||
resolved: (record) => (record.resolved ? formatShortDate(record.resolved) : ""),
|
||||
duration: (record) => (record.resolved ? formatDuration(record.created, record.resolved) : ""),
|
||||
}
|
||||
const csvRows = [Object.keys(cells).join(",")]
|
||||
for (const row of selectedRows) {
|
||||
const r = row.original
|
||||
csvRows.push(
|
||||
Object.values(cells)
|
||||
.map((val) => val(r))
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
const blob = new Blob([csvRows.join("\n")], { type: "text/csv" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "alerts_history.csv"
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="@container w-full">
|
||||
<div className="@3xl:flex items-end mb-4 gap-4">
|
||||
<SectionIntro />
|
||||
<div className="flex items-center gap-2 ms-auto mt-3 @3xl:mt-0">
|
||||
{table.getFilteredSelectedRowModel().rows.length > 0 && (
|
||||
<div className="fixed bottom-0 left-0 w-full p-4 grid grid-cols-2 items-center gap-4 z-50 backdrop-blur-md shrink-0 @lg:static @lg:p-0 @lg:w-auto @lg:gap-3">
|
||||
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteDialogOpen(open)}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="h-9 shrink-0">
|
||||
<Trash2Icon className="size-4 shrink-0" />
|
||||
<span className="ms-1">
|
||||
<Trans>Delete</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Are you sure?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>This will permanently delete all selected records from the database.</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Trans>Continue</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button variant="outline" className="h-10" onClick={handleExportCSV}>
|
||||
<DownloadIcon className="size-4" />
|
||||
<span className="ms-1">
|
||||
<Trans>Export</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="px-4 w-full max-w-full @3xl:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border overflow-x-auto whitespace-nowrap">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="py-3">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
|
||||
<Trans>No results.</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between ps-1 tabular-nums">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
<Trans>
|
||||
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
|
||||
selected.
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit my-3">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||
<Trans>Rows per page</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[4.8em]" id="rows-per-page">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{[10, 20, 50, 100, 200].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
<Trans>
|
||||
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="ms-auto flex items-center gap-2 lg:ms-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden size-9 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<ChevronsLeftIcon className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-9"
|
||||
size="icon"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="size-9"
|
||||
size="icon"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden size-9 lg:flex"
|
||||
size="icon"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<ChevronsRightIcon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useState } from "react"
|
||||
import languages from "@/lib/languages"
|
||||
import { dynamicActivate } from "@/lib/i18n"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
// import { setLang } from "@/lib/i18n"
|
||||
import { Unit } from "@/lib/enums"
|
||||
|
||||
export default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -101,6 +101,87 @@ export default function SettingsProfilePage({ userSettings }: { userSettings: Us
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-1 text-lg font-medium">
|
||||
<Trans>Unit preferences</Trans>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>Change display units for metrics.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="block" htmlFor="unitTemp">
|
||||
<Trans>Temperature unit</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
name="unitTemp"
|
||||
key={userSettings.unitTemp}
|
||||
defaultValue={userSettings.unitTemp?.toString() || String(Unit.Celsius)}
|
||||
>
|
||||
<SelectTrigger id="unitTemp">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={String(Unit.Celsius)}>
|
||||
<Trans>Celsius (°C)</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value={String(Unit.Fahrenheit)}>
|
||||
<Trans>Fahrenheit (°F)</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="block" htmlFor="unitNet">
|
||||
<Trans>Network unit</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
name="unitNet"
|
||||
key={userSettings.unitNet}
|
||||
defaultValue={userSettings.unitNet?.toString() ?? String(Unit.Bytes)}
|
||||
>
|
||||
<SelectTrigger id="unitNet">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={String(Unit.Bytes)}>
|
||||
<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value={String(Unit.Bits)}>
|
||||
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="block" htmlFor="unitDisk">
|
||||
<Trans>Disk unit</Trans>
|
||||
</Label>
|
||||
<Select
|
||||
name="unitDisk"
|
||||
key={userSettings.unitDisk}
|
||||
defaultValue={userSettings.unitDisk?.toString() ?? String(Unit.Bytes)}
|
||||
>
|
||||
<SelectTrigger id="unitDisk">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={String(Unit.Bytes)}>
|
||||
<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>
|
||||
</SelectItem>
|
||||
<SelectItem value={String(Unit.Bits)}>
|
||||
<Trans>Bits (Kbps, Mbps, Gbps)</Trans>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Button type="submit" className="flex items-center gap-1.5 disabled:opacity-100" disabled={isLoading}>
|
||||
{isLoading ? <LoaderCircleIcon className="h-4 w-4 animate-spin" /> : <SaveIcon className="h-4 w-4" />}
|
||||
<Trans>Save Settings</Trans>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { getPagePath, redirectPage } from "@nanostores/router"
|
||||
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon } from "lucide-react"
|
||||
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, LogsIcon } from "lucide-react"
|
||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
import { UserSettings } from "@/types.js"
|
||||
@@ -16,6 +16,7 @@ import Notifications from "./notifications.tsx"
|
||||
import ConfigYaml from "./config-yaml.tsx"
|
||||
import { useLingui } from "@lingui/react/macro"
|
||||
import Fingerprints from "./tokens-fingerprints.tsx"
|
||||
import AlertsHistoryDataTable from "./alerts-history-data-table"
|
||||
|
||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||
try {
|
||||
@@ -63,7 +64,12 @@ export default function SettingsLayout() {
|
||||
title: t`Tokens & Fingerprints`,
|
||||
href: getPagePath($router, "settings", { name: "tokens" }),
|
||||
icon: FingerprintIcon,
|
||||
// admin: true,
|
||||
noReadOnly: true,
|
||||
},
|
||||
{
|
||||
title: t`Alert History`,
|
||||
href: getPagePath($router, "settings", { name: "alert-history" }),
|
||||
icon: LogsIcon,
|
||||
},
|
||||
{
|
||||
title: t`YAML Config`,
|
||||
@@ -95,8 +101,8 @@ export default function SettingsLayout() {
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Separator className="hidden md:block my-5" />
|
||||
<div className="flex flex-col gap-3.5 md:flex-row md:gap-5 lg:gap-10">
|
||||
<aside className="md:max-w-44 min-w-40">
|
||||
<div className="flex flex-col gap-3.5 md:flex-row md:gap-5 lg:gap-12">
|
||||
<aside className="md:max-w-52 min-w-40">
|
||||
<SidebarNav items={sidebarNavItems} />
|
||||
</aside>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -121,5 +127,7 @@ function SettingsContent({ name }: { name: string }) {
|
||||
return <ConfigYaml />
|
||||
case "tokens":
|
||||
return <Fingerprints />
|
||||
case "alert-history":
|
||||
return <AlertsHistoryDataTable />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { cn, isAdmin } from "@/lib/utils"
|
||||
import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils"
|
||||
import { buttonVariants } from "../../ui/button"
|
||||
import { $router, Link, navigate } from "../../router"
|
||||
import { useStore } from "@nanostores/react"
|
||||
@@ -12,6 +12,7 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
title: string
|
||||
icon?: React.FC<React.SVGProps<SVGSVGElement>>
|
||||
admin?: boolean
|
||||
noReadOnly?: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -32,7 +33,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
return (
|
||||
<SelectItem key={item.href} value={item.href}>
|
||||
<span className="flex items-center gap-2 truncate">
|
||||
{item.icon && <item.icon className="h-4 w-4" />}
|
||||
{item.icon && <item.icon className="size-4" />}
|
||||
<span className="truncate">{item.title}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
@@ -44,9 +45,9 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
</div>
|
||||
|
||||
{/* Desktop View */}
|
||||
<nav className={cn("hidden md:grid gap-1", className)} {...props}>
|
||||
<nav className={cn("hidden md:grid gap-1 sticky top-6", className)} {...props}>
|
||||
{items.map((item) => {
|
||||
if (item.admin && !isAdmin()) {
|
||||
if ((item.admin && !isAdmin()) || (item.noReadOnly && isReadOnlyUser())) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
@@ -59,7 +60,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
page?.path === item.href ? "bg-muted hover:bg-muted" : "hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
{item.icon && <item.icon className="h-4 w-4 shrink-0" />}
|
||||
{item.icon && <item.icon className="size-4 shrink-0" />}
|
||||
<span className="truncate">{item.title}</span>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Trans, useLingui } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { $publicKey, pb } from "@/lib/stores"
|
||||
import { memo, useEffect, useMemo, useState } from "react"
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
InstallDropdown,
|
||||
} from "@/components/install-dropdowns"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { $router } from "@/components/router"
|
||||
|
||||
const pbFingerprintOptions = {
|
||||
expand: "system",
|
||||
@@ -41,6 +43,9 @@ const pbFingerprintOptions = {
|
||||
}
|
||||
|
||||
const SettingsFingerprintsPage = memo(() => {
|
||||
if (isReadOnlyUser()) {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
const [fingerprints, setFingerprints] = useState<FingerprintRecord[]>([])
|
||||
|
||||
// Get fingerprint records on mount
|
||||
@@ -154,7 +159,7 @@ const SectionUniversalToken = memo(() => {
|
||||
or on hub restart.
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="min-h-16 overflow-auto max-w-full inline-flex items-center gap-5 mt-3 border py-2 pl-5 pr-4 rounded-md">
|
||||
<div className="min-h-16 overflow-auto max-w-full inline-flex items-center gap-5 mt-3 border py-2 ps-5 pe-4 rounded-md">
|
||||
{!isLoading && (
|
||||
<>
|
||||
<Switch
|
||||
@@ -180,32 +185,33 @@ const SectionUniversalToken = memo(() => {
|
||||
})
|
||||
|
||||
const ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; checked: boolean }) => {
|
||||
const { t } = useLingui()
|
||||
const publicKey = $publicKey.get()
|
||||
const port = "45876"
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{
|
||||
text: "Copy Docker Compose",
|
||||
text: t({ message: "Copy docker compose", context: "Button to copy docker compose file content" }),
|
||||
onClick: () => copyDockerCompose(port, publicKey, token),
|
||||
icons: [DockerIcon],
|
||||
},
|
||||
{
|
||||
text: "Copy Docker Run",
|
||||
text: t({ message: "Copy docker run", context: "Button to copy docker run command" }),
|
||||
onClick: () => copyDockerRun(port, publicKey, token),
|
||||
icons: [DockerIcon],
|
||||
},
|
||||
{
|
||||
text: "Copy Linux Command",
|
||||
text: t`Copy Linux command`,
|
||||
onClick: () => copyLinuxCommand(port, publicKey, token),
|
||||
icons: [TuxIcon],
|
||||
},
|
||||
{
|
||||
text: "Copy Brew Command",
|
||||
text: t({ message: "Homebrew command", context: "Button to copy install command" }),
|
||||
onClick: () => copyLinuxCommand(port, publicKey, token, true),
|
||||
icons: [TuxIcon, AppleIcon],
|
||||
},
|
||||
{
|
||||
text: "Copy Windows Command",
|
||||
text: t({ message: "Windows command", context: "Button to copy install command" }),
|
||||
onClick: () => copyWindowsCommand(port, publicKey, token),
|
||||
icons: [WindowsIcon],
|
||||
},
|
||||
@@ -233,26 +239,28 @@ const ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; c
|
||||
})
|
||||
|
||||
const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRecord[] }) => {
|
||||
const { t } = useLingui()
|
||||
const isReadOnly = isReadOnlyUser()
|
||||
|
||||
const headerCols = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "System",
|
||||
label: t`System`,
|
||||
Icon: ServerIcon,
|
||||
w: "11em",
|
||||
},
|
||||
{
|
||||
label: "Token",
|
||||
label: t`Token`,
|
||||
Icon: KeyIcon,
|
||||
w: "20em",
|
||||
},
|
||||
{
|
||||
label: "Fingerprint",
|
||||
label: t`Fingerprint`,
|
||||
Icon: FingerprintIcon,
|
||||
w: "20em",
|
||||
},
|
||||
],
|
||||
[]
|
||||
[t]
|
||||
)
|
||||
return (
|
||||
<div className="rounded-md border overflow-hidden w-full mt-4">
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
$temperatureFilter,
|
||||
} from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import { ChartType, Os } from "@/lib/enums"
|
||||
import { ChartType, Unit, Os } from "@/lib/enums"
|
||||
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||
import { useStore } from "@nanostores/react"
|
||||
@@ -21,9 +21,10 @@ import ChartTimeSelect from "../charts/chart-time-select"
|
||||
import {
|
||||
chartTimeData,
|
||||
cn,
|
||||
decimalString,
|
||||
formatBytes,
|
||||
getHostDisplayValue,
|
||||
getPbTimestamp,
|
||||
getSizeAndUnit,
|
||||
listen,
|
||||
toFixedFloat,
|
||||
useLocalStorage,
|
||||
@@ -47,6 +48,7 @@ const DiskChart = lazy(() => import("../charts/disk-chart"))
|
||||
const SwapChart = lazy(() => import("../charts/swap-chart"))
|
||||
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
|
||||
const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
|
||||
const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
|
||||
|
||||
const cache = new Map<string, any>()
|
||||
|
||||
@@ -131,6 +133,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
const [bottomSpacing, setBottomSpacing] = useState(0)
|
||||
const [chartLoading, setChartLoading] = useState(true)
|
||||
const isLongerChart = chartTime !== "1h"
|
||||
const userSettings = $userSettings.get()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${name} / Beszel`
|
||||
@@ -472,7 +475,13 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
description={t`Average system-wide CPU utilization`}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={maxValues} unit="%" />
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="CPU Usage"
|
||||
maxToggled={maxValues}
|
||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && (
|
||||
@@ -519,7 +528,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
description={t`Throughput of root filesystem`}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName="dio" maxToggled={maxValues} />
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="dio"
|
||||
maxToggled={maxValues}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
@@ -529,7 +550,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
cornerEl={maxValSelect}
|
||||
description={t`Network traffic of public interfaces`}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName="bw" maxToggled={maxValues} />
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName="bw"
|
||||
maxToggled={maxValues}
|
||||
tickFormatter={(val) => {
|
||||
let { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && containerData.length > 0 && (
|
||||
@@ -563,6 +596,18 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{/* Load Average chart */}
|
||||
{system.info.l1 !== undefined && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Load Average`}
|
||||
description={t`System load averages over time`}
|
||||
>
|
||||
<LoadAverageChart chartData={chartData} />
|
||||
</ChartCard>
|
||||
)}
|
||||
|
||||
{/* Temperature chart */}
|
||||
{systemStats.at(-1)?.stats.t && (
|
||||
<ChartCard
|
||||
@@ -594,10 +639,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
<div className="grid xl:grid-cols-2 gap-4">
|
||||
{Object.keys(systemStats.at(-1)?.stats.g ?? {}).map((id) => {
|
||||
const gpu = systemStats.at(-1)?.stats.g?.[id] as GPUData
|
||||
const sizeFormatter = (value: number, decimals?: number) => {
|
||||
const { v, u } = getSizeAndUnit(value, false)
|
||||
return toFixedFloat(v, decimals || 1) + u
|
||||
}
|
||||
return (
|
||||
<div key={id} className="contents">
|
||||
<ChartCard
|
||||
@@ -606,7 +647,12 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
title={`${gpu.n} ${t`Usage`}`}
|
||||
description={t`Average utilization of ${gpu.n}`}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName={`g.${id}.u`} unit="%" />
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`g.${id}.u`}
|
||||
tickFormatter={(val) => toFixedFloat(val, 2) + "%"}
|
||||
contentFormatter={({ value }) => decimalString(value) + "%"}
|
||||
/>
|
||||
</ChartCard>
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
@@ -618,8 +664,14 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
chartData={chartData}
|
||||
chartName={`g.${id}.mu`}
|
||||
max={gpu.mt}
|
||||
tickFormatter={sizeFormatter}
|
||||
contentFormatter={(value) => sizeFormatter(value, 2)}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)
|
||||
return decimalString(convertedValue) + " " + unit
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
</div>
|
||||
@@ -653,7 +705,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
description={t`Throughput of ${extraFsName}`}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName={`efs.${extraFsName}`} maxToggled={maxValues} />
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
chartName={`efs.${extraFsName}`}
|
||||
maxToggled={maxValues}
|
||||
tickFormatter={(val) => {
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitDisk, true)
|
||||
return toFixedFloat(value, value >= 10 ? 0 : 1) + " " + unit
|
||||
}}
|
||||
contentFormatter={({ value }) => {
|
||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, true)
|
||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||
}}
|
||||
/>
|
||||
</ChartCard>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -63,12 +63,20 @@ import {
|
||||
PenBoxIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $systems, pb } from "@/lib/stores"
|
||||
import { $systems, $userSettings, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||
import {
|
||||
cn,
|
||||
copyToClipboard,
|
||||
isReadOnlyUser,
|
||||
useLocalStorage,
|
||||
formatTemperature,
|
||||
decimalString,
|
||||
formatBytes,
|
||||
} from "@/lib/utils"
|
||||
import AlertsButton from "../alerts/alert-button"
|
||||
import { $router, Link, navigate } from "../router"
|
||||
import { EthernetIcon, GpuIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { useLingui, Trans } from "@lingui/react/macro"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { Input } from "../ui/input"
|
||||
@@ -83,8 +91,8 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
const val = (info.getValue() as number) || 0
|
||||
return (
|
||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||
<span className="min-w-[3.3em]">{decimalString(val, 1)}%</span>
|
||||
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
||||
<span className="min-w-8">{decimalString(val, 1)}%</span>
|
||||
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-0 w-full h-full origin-left",
|
||||
@@ -144,7 +152,6 @@ export default function SystemsTable() {
|
||||
}
|
||||
return [
|
||||
{
|
||||
// size: 200,
|
||||
size: 200,
|
||||
minSize: 0,
|
||||
accessorKey: "name",
|
||||
@@ -163,9 +170,10 @@ export default function SystemsTable() {
|
||||
return false
|
||||
},
|
||||
enableHiding: false,
|
||||
invertSorting: false,
|
||||
Icon: ServerIcon,
|
||||
cell: (info) => (
|
||||
<span className="flex gap-0.5 items-center text-base md:pe-5">
|
||||
<span className="flex gap-0.5 items-center text-base md:ps-1 md:pe-5">
|
||||
<IndicatorDot system={info.row.original} />
|
||||
<Button
|
||||
data-nolink
|
||||
@@ -174,35 +182,33 @@ export default function SystemsTable() {
|
||||
onClick={() => copyToClipboard(info.getValue() as string)}
|
||||
>
|
||||
{info.getValue() as string}
|
||||
<CopyIcon className="h-2.5 w-2.5" />
|
||||
<CopyIcon className="size-2.5" />
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.cpu",
|
||||
accessorFn: ({ info }) => decimalString(info.cpu, info.cpu >= 10 ? 1 : 2),
|
||||
id: "cpu",
|
||||
name: () => t`CPU`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: CpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.mp",
|
||||
// accessorKey: "info.mp",
|
||||
accessorFn: (originalRow) => originalRow.info.mp,
|
||||
id: "memory",
|
||||
name: () => t`Memory`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: MemoryStickIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.dp",
|
||||
accessorFn: (originalRow) => originalRow.info.dp,
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
Icon: HardDriveIcon,
|
||||
header: sortableHeader,
|
||||
@@ -211,29 +217,65 @@ export default function SystemsTable() {
|
||||
accessorFn: (originalRow) => originalRow.info.g,
|
||||
id: "gpu",
|
||||
name: () => "GPU",
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
cell: CellFormatter,
|
||||
Icon: GpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
id: "loadAverage",
|
||||
accessorFn: ({ info }) => {
|
||||
const { l1 = 0, l5 = 0, l15 = 0 } = info
|
||||
return l1 + l5 + l15
|
||||
},
|
||||
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
|
||||
size: 0,
|
||||
Icon: HourglassIcon,
|
||||
header: sortableHeader,
|
||||
cell(info: CellContext<SystemRecord, unknown>) {
|
||||
const { info: sysInfo, status } = info.row.original
|
||||
if (sysInfo.l1 === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { l1 = 0, l5 = 0, l15 = 0, t: cpuThreads = 1 } = sysInfo
|
||||
const loadAverages = [l1, l5, l15]
|
||||
|
||||
function getDotColor() {
|
||||
const max = Math.max(...loadAverages)
|
||||
const normalized = max / cpuThreads
|
||||
if (status !== "up") return "bg-primary/30"
|
||||
if (normalized < 0.7) return "bg-green-500"
|
||||
if (normalized < 1) return "bg-yellow-500"
|
||||
return "bg-red-600"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
|
||||
<span className={cn("inline-block size-2 rounded-full me-0.5", getDotColor())} />
|
||||
{loadAverages.map((la, i) => (
|
||||
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||
id: "net",
|
||||
name: () => t`Net`,
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
size: 0,
|
||||
Icon: EthernetIcon,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
if (info.row.original.status === "paused") {
|
||||
return null
|
||||
}
|
||||
const val = info.getValue() as number
|
||||
const userSettings = useStore($userSettings)
|
||||
const { value, unit } = formatBytes(val, true, userSettings.unitNet, true)
|
||||
return (
|
||||
<span
|
||||
className={cn("tabular-nums whitespace-nowrap", {
|
||||
"ps-1": viewMode === "table",
|
||||
})}
|
||||
>
|
||||
{decimalString(val, val >= 100 ? 1 : 2)} MB/s
|
||||
<span className="tabular-nums whitespace-nowrap">
|
||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
@@ -242,8 +284,6 @@ export default function SystemsTable() {
|
||||
accessorFn: (originalRow) => originalRow.info.dt,
|
||||
id: "temp",
|
||||
name: () => t({ message: "Temp", comment: "Temperature label in systems table" }),
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
size: 50,
|
||||
hideSort: true,
|
||||
Icon: ThermometerIcon,
|
||||
@@ -253,22 +293,20 @@ export default function SystemsTable() {
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
const userSettings = useStore($userSettings)
|
||||
const { value, unit } = formatTemperature(val, userSettings.unitTemp)
|
||||
return (
|
||||
<span
|
||||
className={cn("tabular-nums whitespace-nowrap", {
|
||||
"ps-1.5": viewMode === "table",
|
||||
})}
|
||||
>
|
||||
{decimalString(val)} °C
|
||||
<span className={cn("tabular-nums whitespace-nowrap", viewMode === "table" && "ps-0.5")}>
|
||||
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "info.v",
|
||||
accessorFn: (originalRow) => originalRow.info.v,
|
||||
id: "agent",
|
||||
name: () => t`Agent`,
|
||||
invertSorting: true,
|
||||
// invertSorting: true,
|
||||
size: 50,
|
||||
Icon: WifiIcon,
|
||||
hideSort: true,
|
||||
@@ -280,11 +318,7 @@ export default function SystemsTable() {
|
||||
}
|
||||
const system = info.row.original
|
||||
return (
|
||||
<span
|
||||
className={cn("flex gap-2 items-center md:pe-5 tabular-nums", {
|
||||
"ps-1": viewMode === "table",
|
||||
})}
|
||||
>
|
||||
<span className={cn("flex gap-2 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
|
||||
<IndicatorDot
|
||||
system={system}
|
||||
className={
|
||||
@@ -304,7 +338,7 @@ export default function SystemsTable() {
|
||||
name: () => t({ message: "Actions", comment: "Table column" }),
|
||||
size: 50,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end items-center gap-1">
|
||||
<div className="flex justify-end items-center gap-1 -ms-3">
|
||||
<AlertsButton system={row.original} />
|
||||
<ActionsButton system={row.original} />
|
||||
</div>
|
||||
@@ -328,6 +362,9 @@ export default function SystemsTable() {
|
||||
columnVisibility,
|
||||
},
|
||||
defaultColumn: {
|
||||
// sortDescFirst: true,
|
||||
invertSorting: true,
|
||||
sortUndefined: "last",
|
||||
minSize: 0,
|
||||
size: 900,
|
||||
maxSize: 900,
|
||||
@@ -511,7 +548,7 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead className="px-2" key={header.id}>
|
||||
<TableHead className="px-1.5" key={header.id}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
@@ -11,13 +13,14 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-[.3em] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
"peer size-4 flex items-center justify-center shrink-0 rounded-[.3em] border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
<Check className="size-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
|
||||
49
beszel/site/src/components/ui/collapsible.tsx
Normal file
49
beszel/site/src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon, HourglassIcon } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "./button"
|
||||
|
||||
interface CollapsibleProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
defaultOpen?: boolean
|
||||
className?: string
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Collapsible({ title, children, description, defaultOpen = false, className, icon }: CollapsibleProps) {
|
||||
const [isOpen, setIsOpen] = React.useState(defaultOpen)
|
||||
|
||||
return (
|
||||
<div className={cn("border rounded-lg", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between p-4 font-semibold"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={cn("h-4 w-4 transition-transform duration-200", {
|
||||
"rotate-180": isOpen,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
{description && (
|
||||
<div className="px-4 pb-2 text-sm text-muted-foreground">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4">
|
||||
<div className="grid gap-3">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -121,3 +121,12 @@ export function GpuIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Remix icons (Apache 2.0) https://github.com/Remix-Design/RemixIcon/blob/master/License
|
||||
export function HourglassIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...props} fill="currentColor">
|
||||
<path d="M4 2h16v4.5L13.5 12l6.5 5.5V22H4v-4.5l6.5-5.5L4 6.5zm12.3 5L18 5.5V4H6v1.5L7.7 7zM12 13.3l-6 5.2V20h1l5-3 5 3h1v-1.5z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:bg-muted",
|
||||
"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:!bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Operating system */
|
||||
export enum Os {
|
||||
Linux = 0,
|
||||
Darwin,
|
||||
@@ -5,9 +6,18 @@ export enum Os {
|
||||
FreeBSD,
|
||||
}
|
||||
|
||||
/** Type of chart */
|
||||
export enum ChartType {
|
||||
Memory,
|
||||
Disk,
|
||||
Network,
|
||||
CPU,
|
||||
}
|
||||
|
||||
/** Unit of measurement */
|
||||
export enum Unit {
|
||||
Bytes,
|
||||
Bits,
|
||||
Celsius,
|
||||
Fahrenheit,
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export const $maxValues = atom(false)
|
||||
export const $userSettings = map<UserSettings>({
|
||||
chartTime: "1h",
|
||||
emails: [pb.authStore.record?.email || ""],
|
||||
// unitTemp: "celsius",
|
||||
// unitNet: "mbps",
|
||||
// unitDisk: "mbps",
|
||||
})
|
||||
// update local storage on change
|
||||
$userSettings.subscribe((value) => {
|
||||
|
||||
@@ -3,14 +3,23 @@ import { toast } from "@/components/ui/use-toast"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
|
||||
import { AlertInfo, AlertRecord, ChartTimeData, ChartTimes, FingerprintRecord, SystemRecord } from "@/types"
|
||||
import {
|
||||
AlertInfo,
|
||||
AlertRecord,
|
||||
ChartTimeData,
|
||||
ChartTimes,
|
||||
FingerprintRecord,
|
||||
SystemRecord,
|
||||
UserSettings,
|
||||
} from "@/types"
|
||||
import { RecordModel, RecordSubscription } from "pocketbase"
|
||||
import { WritableAtom } from "nanostores"
|
||||
import { timeDay, timeHour } from "d3-time"
|
||||
import { useEffect, useState } from "react"
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||
import { EthernetIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||
import { EthernetIcon, HourglassIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
import { Unit } from "./enums"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -73,7 +82,10 @@ export const updateSystemList = (() => {
|
||||
|
||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||
export async function logOut() {
|
||||
sessionStorage.setItem("lo", "t")
|
||||
$systems.set([])
|
||||
$alerts.set([])
|
||||
$userSettings.set({} as UserSettings)
|
||||
sessionStorage.setItem("lo", "t") // prevent auto login on logout
|
||||
pb.authStore.clear()
|
||||
pb.realtime.unsubscribe()
|
||||
}
|
||||
@@ -225,17 +237,17 @@ export function useYAxisWidth() {
|
||||
return { yAxisWidth, updateYAxisWidth }
|
||||
}
|
||||
|
||||
export function toFixedWithoutTrailingZeros(num: number, digits: number) {
|
||||
return parseFloat(num.toFixed(digits)).toString()
|
||||
}
|
||||
|
||||
/** Format number to x decimal places, without trailing zeros */
|
||||
export function toFixedFloat(num: number, digits: number) {
|
||||
return parseFloat(num.toFixed(digits))
|
||||
return parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))
|
||||
}
|
||||
|
||||
let decimalFormatters: Map<number, Intl.NumberFormat> = new Map()
|
||||
/** Format number to x decimal places */
|
||||
/** Format number to x decimal places, maintaining trailing zeros */
|
||||
export function decimalString(num: number, digits = 2) {
|
||||
if (digits === 0) {
|
||||
return Math.ceil(num).toString()
|
||||
}
|
||||
let formatter = decimalFormatters.get(digits)
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(undefined, {
|
||||
@@ -266,42 +278,96 @@ export function useLocalStorage<T>(key: string, defaultValue: T) {
|
||||
return [value, setValue]
|
||||
}
|
||||
|
||||
/** Format temperature to user's preferred unit */
|
||||
export function formatTemperature(celsius: number, unit?: Unit): { value: number; unit: string } {
|
||||
if (!unit) {
|
||||
unit = $userSettings.get().unitTemp || Unit.Celsius
|
||||
}
|
||||
// need loose equality check due to form data being strings
|
||||
if (unit == Unit.Fahrenheit) {
|
||||
return {
|
||||
value: celsius * 1.8 + 32,
|
||||
unit: "°F",
|
||||
}
|
||||
}
|
||||
return {
|
||||
value: celsius,
|
||||
unit: "°C",
|
||||
}
|
||||
}
|
||||
|
||||
/** Format bytes to user's preferred unit */
|
||||
export function formatBytes(
|
||||
size: number,
|
||||
perSecond = false,
|
||||
unit = Unit.Bytes,
|
||||
isMegabytes = false
|
||||
): { value: number; unit: string } {
|
||||
// Convert MB to bytes if isMegabytes is true
|
||||
if (isMegabytes) size *= 1024 * 1024
|
||||
|
||||
// need loose equality check due to form data being strings
|
||||
if (unit == Unit.Bits) {
|
||||
const bits = size * 8
|
||||
const suffix = perSecond ? "ps" : ""
|
||||
if (bits < 1000) return { value: bits, unit: `b${suffix}` }
|
||||
if (bits < 1_000_000) return { value: bits / 1_000, unit: `Kb${suffix}` }
|
||||
if (bits < 1_000_000_000)
|
||||
return {
|
||||
value: bits / 1_000_000,
|
||||
unit: `Mb${suffix}`,
|
||||
}
|
||||
if (bits < 1_000_000_000_000)
|
||||
return {
|
||||
value: bits / 1_000_000_000,
|
||||
unit: `Gb${suffix}`,
|
||||
}
|
||||
return {
|
||||
value: bits / 1_000_000_000_000,
|
||||
unit: `Tb${suffix}`,
|
||||
}
|
||||
}
|
||||
// bytes
|
||||
const suffix = perSecond ? "/s" : ""
|
||||
if (size < 100) return { value: size, unit: `B${suffix}` }
|
||||
if (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` }
|
||||
if (size < 1000 * 1024 ** 2)
|
||||
return {
|
||||
value: size / 1024 ** 2,
|
||||
unit: `MB${suffix}`,
|
||||
}
|
||||
if (size < 1000 * 1024 ** 3)
|
||||
return {
|
||||
value: size / 1024 ** 3,
|
||||
unit: `GB${suffix}`,
|
||||
}
|
||||
return {
|
||||
value: size / 1024 ** 4,
|
||||
unit: `TB${suffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch or create user settings in database */
|
||||
export async function updateUserSettings() {
|
||||
try {
|
||||
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
|
||||
$userSettings.set(req.settings)
|
||||
return
|
||||
} catch (e) {
|
||||
console.log("get settings", e)
|
||||
console.error("get settings", e)
|
||||
}
|
||||
// create user settings if error fetching existing
|
||||
try {
|
||||
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
|
||||
$userSettings.set(createdSettings.settings)
|
||||
} catch (e) {
|
||||
console.log("create settings", e)
|
||||
console.error("create settings", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value and unit of size (TB, GB, or MB) for a given size
|
||||
* @param n size in gigabytes or megabytes
|
||||
* @param isGigabytes boolean indicating if n represents gigabytes (true) or megabytes (false)
|
||||
* @returns an object containing the value and unit of size
|
||||
*/
|
||||
export const getSizeAndUnit = (n: number, isGigabytes = true) => {
|
||||
const sizeInGB = isGigabytes ? n : n / 1_000
|
||||
|
||||
if (sizeInGB >= 1_000) {
|
||||
return { v: sizeInGB / 1_000, u: " TB" }
|
||||
} else if (sizeInGB >= 1) {
|
||||
return { v: sizeInGB, u: " GB" }
|
||||
}
|
||||
return { v: isGigabytes ? sizeInGB * 1_000 : n, u: " MB" }
|
||||
}
|
||||
|
||||
export const chartMargin = { top: 12 }
|
||||
|
||||
/** Alert info for each alert type */
|
||||
export const alertInfo: Record<string, AlertInfo> = {
|
||||
Status: {
|
||||
name: () => t`Status`,
|
||||
@@ -342,6 +408,36 @@ export const alertInfo: Record<string, AlertInfo> = {
|
||||
icon: ThermometerIcon,
|
||||
desc: () => t`Triggers when any sensor exceeds a threshold`,
|
||||
},
|
||||
LoadAvg1: {
|
||||
name: () => t`Load Average 1m`,
|
||||
unit: "",
|
||||
icon: HourglassIcon,
|
||||
max: 100,
|
||||
min: 0.1,
|
||||
start: 10,
|
||||
step: 0.1,
|
||||
desc: () => t`Triggers when 1 minute load average exceeds a threshold`,
|
||||
},
|
||||
LoadAvg5: {
|
||||
name: () => t`Load Average 5m`,
|
||||
unit: "",
|
||||
icon: HourglassIcon,
|
||||
max: 100,
|
||||
min: 0.1,
|
||||
start: 10,
|
||||
step: 0.1,
|
||||
desc: () => t`Triggers when 5 minute load average exceeds a threshold`,
|
||||
},
|
||||
LoadAvg15: {
|
||||
name: () => t`Load Average 15m`,
|
||||
unit: "",
|
||||
icon: HourglassIcon,
|
||||
min: 0.1,
|
||||
max: 100,
|
||||
start: 10,
|
||||
step: 0.1,
|
||||
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,3 +456,42 @@ export const getHubURL = () => BESZEL?.HUB_URL || window.location.origin
|
||||
|
||||
/** Map of system IDs to their corresponding tokens (used to avoid fetching in add-system dialog) */
|
||||
export const tokenMap = new Map<SystemRecord["id"], FingerprintRecord["token"]>()
|
||||
|
||||
/** Calculate duration between two dates and format as human-readable string */
|
||||
export function formatDuration(
|
||||
createdDate: string | null | undefined,
|
||||
resolvedDate: string | null | undefined
|
||||
): string {
|
||||
const created = createdDate ? new Date(createdDate) : null
|
||||
const resolved = resolvedDate ? new Date(resolvedDate) : null
|
||||
|
||||
if (!created || !resolved) return ""
|
||||
|
||||
const diffMs = resolved.getTime() - created.getTime()
|
||||
if (diffMs < 0) return ""
|
||||
|
||||
const totalSeconds = Math.floor(diffMs / 1000)
|
||||
let hours = Math.floor(totalSeconds / 3600)
|
||||
let minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
let seconds = totalSeconds % 60
|
||||
|
||||
// if seconds are close to 60, round up to next minute
|
||||
// if minutes are close to 60, round up to next hour
|
||||
if (seconds >= 58) {
|
||||
minutes += 1
|
||||
seconds = 0
|
||||
}
|
||||
if (minutes >= 60) {
|
||||
hours += 1
|
||||
minutes = 0
|
||||
}
|
||||
|
||||
// For durations over 1 hour, omit seconds for cleaner display
|
||||
if (hours > 0) {
|
||||
return [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null].filter(Boolean).join(" ")
|
||||
}
|
||||
|
||||
return [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null, seconds ? `${seconds}s` : null]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "تم النسخ إلى الحافظة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "نسخ docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "نسخ docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "نسخ المضيف"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "نسخ أمر لينكس"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "فشل في تحديث التنبيه"
|
||||
msgid "Filter..."
|
||||
msgstr "تصفية..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "شبكة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "أمر Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "عنوان البريد الإشباكي غير صالح."
|
||||
msgid "Kernel"
|
||||
msgstr "النواة"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "اللغة"
|
||||
@@ -453,6 +471,14 @@ msgstr "التخطيط"
|
||||
msgid "Light"
|
||||
msgstr "فاتح"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "تسجيل الخروج"
|
||||
@@ -734,6 +760,7 @@ msgstr "استخدام التبديل"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "النظام"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "تبديل السمة"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "رمز مميز"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "تسمح الرموز المميزة للوكلاء بالاتصال و
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق. ينتهي بعد ساعة واحدة أو عند إعادة تشغيل المحور."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "أمر Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Записано в клипборда"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Копирай docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Копирай docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Копирай хоста"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Копирай linux командата"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Неуспешно обнови тревога"
|
||||
msgid "Filter..."
|
||||
msgstr "Филтрирай..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "За <0>{min}</0> {min, plural, one {минута} other {минути}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Мрежово"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Команда Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Невалиден имейл адрес."
|
||||
msgid "Kernel"
|
||||
msgstr "Linux Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Език"
|
||||
@@ -453,6 +471,14 @@ msgstr "Подреждане"
|
||||
msgid "Light"
|
||||
msgstr "Светъл"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Изход"
|
||||
@@ -734,6 +760,7 @@ msgstr "Използване на swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Включи тема"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Задейства се, когато някой даден сензор надвиши зададен праг"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Команда Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Zkopírováno do schránky"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopírovat docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr ""
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopírovat hostitele"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopírovat příkaz Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Nepodařilo se aktualizovat upozornění"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtr..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Mřížka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr ""
|
||||
@@ -440,6 +448,16 @@ msgstr "Neplatná e-mailová adresa."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jazyk"
|
||||
@@ -453,6 +471,14 @@ msgstr "Rozvržení"
|
||||
msgid "Light"
|
||||
msgstr "Světlý"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odhlásit"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap využití"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Systém"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Přepnout motiv"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Spustí se, když některý senzor překročí prahovou hodnotu"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr ""
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopieret til udklipsholder"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiér docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiér docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopier host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopier Linux kommando"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Kunne ikke opdatere alarm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "For <0>{min}</0> {min, plural, one {minut} other {minutter}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Gitter"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ugyldig email adresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprog"
|
||||
@@ -453,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Lys"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Log ud"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap forbrug"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Skift tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Udløser når en sensor overstiger en tærskel"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "In die Zwischenablage kopiert"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopieren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopieren"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Host kopieren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux-Befehl kopieren"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Warnung konnte nicht aktualisiert werden"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Raster"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-Befehl"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ungültige E-Mail-Adresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
@@ -453,6 +471,14 @@ msgstr "Anordnung"
|
||||
msgid "Light"
|
||||
msgstr "Hell"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Abmelden"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap-Nutzung"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Darstellung umschalten"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fi
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Wenn aktiviert, ermöglicht dieser Token Agents, sich selbst zu registrieren, ohne vorherige Systemerstellung. Läuft nach einer Stunde oder beim Hub-Neustart ab."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-Befehl"
|
||||
|
||||
@@ -206,11 +206,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copied to clipboard"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copy docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copy docker run"
|
||||
@@ -225,6 +227,7 @@ msgid "Copy host"
|
||||
msgstr "Copy host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copy Linux command"
|
||||
|
||||
@@ -391,6 +394,10 @@ msgstr "Failed to update alert"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr "Fingerprint"
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -414,6 +421,7 @@ msgid "Grid"
|
||||
msgstr "Grid"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew command"
|
||||
@@ -435,6 +443,16 @@ msgstr "Invalid email address."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr "L15"
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr "L5"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Language"
|
||||
@@ -448,6 +466,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Light"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr "Load Average 15m"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr "Load Average 5m"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Log Out"
|
||||
@@ -729,6 +755,7 @@ msgstr "Swap Usage"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -796,6 +823,7 @@ msgid "Toggle theme"
|
||||
msgstr "Toggle theme"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -813,6 +841,14 @@ msgstr "Tokens allow agents to connect and register. Fingerprints are stable ide
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr "Triggers when 15 minute load average exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr "Triggers when 5 minute load average exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggers when any sensor exceeds a threshold"
|
||||
@@ -901,6 +937,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows command"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiado al portapapeles"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copiar docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copiar docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Copiar host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copiar comando de Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Error al actualizar la alerta"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrar..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Cuadrícula"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Dirección de correo electrónico no válida."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
@@ -453,6 +471,14 @@ msgstr "Diseño"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Cerrar Sesión"
|
||||
@@ -734,6 +760,7 @@ msgstr "Uso de Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Alternar tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Los tokens permiten que los agentes se conecten y registren. Las huellas
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Se activa cuando cualquier sensor supera un umbral"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Cuando está habilitado, este token permite que los agentes se auto-registren sin crear previamente el sistema. Expira después de una hora o al reiniciar el hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "در کلیپبورد کپی شد"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "کپی docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "کپی docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "کپی میزبان"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "کپی دستور لینوکس"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "بهروزرسانی هشدار ناموفق بود"
|
||||
msgid "Filter..."
|
||||
msgstr "فیلتر..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "برای <0>{min}</0> {min, plural, one {دقیقه} other {دقیقه}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "جدول"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "دستور Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "آدرس ایمیل نامعتبر است."
|
||||
msgid "Kernel"
|
||||
msgstr "هسته"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "زبان"
|
||||
@@ -453,6 +471,14 @@ msgstr "طرحبندی"
|
||||
msgid "Light"
|
||||
msgstr "روشن"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "خروج"
|
||||
@@ -734,6 +760,7 @@ msgstr "میزان استفاده از Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "سیستم"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "تغییر تم"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "توکن"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "توکنها به عاملها اجازه اتصال و ثبت
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "توکنها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده میشوند."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "هنگامی که هر حسگری از یک آستانه فراتر رود، فعال میشود"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "هنگامی که فعال است، این توکن به عاملها اجازه خودثبتنامی بدون ایجاد سیستم قبلی میدهد. پس از یک ساعت یا در راهاندازی مجدد هاب منقضی میشود."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "دستور Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copié dans le presse-papiers"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copier docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copier docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Copier l'hôte"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copier la commande Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Échec de la mise à jour de l'alerte"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrer..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Pour <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Grille"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Commande Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Adresse email invalide."
|
||||
msgid "Kernel"
|
||||
msgstr "Noyau"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Langue"
|
||||
@@ -453,6 +471,14 @@ msgstr "Disposition"
|
||||
msgid "Light"
|
||||
msgstr "Clair"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Déconnexion"
|
||||
@@ -734,6 +760,7 @@ msgstr "Utilisation du swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Système"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Changer le thème"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Les tokens permettent aux agents de se connecter et de s'enregistrer. Le
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Déclenchement lorsque tout capteur dépasse un seuil"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Lorsqu'il est activé, ce token permet aux agents de s'auto-enregistrer sans création préalable du système. Expire après une heure ou au redémarrage du hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Commande Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopirano u međuspremnik"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiraj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiraj docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopiraj hosta"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiraj Linux komandu"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Ažuriranje upozorenja nije uspjelo"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minutu} other {minute}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew naredba"
|
||||
@@ -440,6 +448,16 @@ msgstr "Nevažeća adresa e-pošte."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jezik"
|
||||
@@ -453,6 +471,14 @@ msgstr "Izgled"
|
||||
msgid "Light"
|
||||
msgstr "Svijetlo"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odjava"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap Iskorištenost"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistem"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Uključi/isključi temu"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Pokreće se kada bilo koji senzor prijeđe prag"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows naredba"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Vágólapra másolva"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose másolása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run másolása"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Hoszt másolása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux parancs másolása"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Nem sikerült frissíteni a riasztást"
|
||||
msgid "Filter..."
|
||||
msgstr "Szűrő..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "A <0>{min}</0> {min, plural, one {perc} other {percek}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rács"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew parancs"
|
||||
@@ -440,6 +448,16 @@ msgstr "Érvénytelen e-mail cím."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Nyelv"
|
||||
@@ -453,6 +471,14 @@ msgstr "Elrendezés"
|
||||
msgid "Light"
|
||||
msgstr "Világos"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Kijelentkezés"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap használat"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Rendszer"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Téma váltása"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Bekapcsol, ha bármelyik érzékelő túllép egy küszöbértéket"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows parancs"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Afritað í klippiborð"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Afrita docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Afrita docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Afrita host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Afrita Linux aðgerð"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Mistókst að uppfæra tilkynningu"
|
||||
msgid "Filter..."
|
||||
msgstr "Sía..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr ""
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew skipun"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ógilt netfang."
|
||||
msgid "Kernel"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Tungumál"
|
||||
@@ -453,6 +471,14 @@ msgstr ""
|
||||
msgid "Light"
|
||||
msgstr "Ljóst"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Útskrá"
|
||||
@@ -734,6 +760,7 @@ msgstr "Skipti minni"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Kerfi"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Velja þema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Virkjast þegar einhver skynjari fer yfir þröskuld"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows skipun"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiato negli appunti"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copia docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copia docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Copia host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copia comando Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Aggiornamento dell'avviso fallito"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtra..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Per <0>{min}</0> {min, plural, one {minuto} other {minuti}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Griglia"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Indirizzo email non valido."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Lingua"
|
||||
@@ -453,6 +471,14 @@ msgstr "Aspetto"
|
||||
msgid "Light"
|
||||
msgstr "Chiaro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Disconnetti"
|
||||
@@ -734,6 +760,7 @@ msgstr "Utilizzo Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Attiva/disattiva tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "I token consentono agli agenti di connettersi e registrarsi. Le impronte
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Attiva quando un sensore supera una soglia"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Quando abilitato, questo token consente agli agenti di auto-registrarsi senza creazione preventiva del sistema. Scade dopo un'ora o al riavvio dell'hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "クリップボードにコピーされました"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "docker compose をコピー"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "docker run をコピー"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "ホストをコピー"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linuxコマンドをコピー"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "アラートの更新に失敗しました"
|
||||
msgid "Filter..."
|
||||
msgstr "フィルター..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {分} other {分}}の間"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "グリッド"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew コマンド"
|
||||
@@ -440,6 +448,16 @@ msgstr "無効なメールアドレスです。"
|
||||
msgid "Kernel"
|
||||
msgstr "カーネル"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "言語"
|
||||
@@ -453,6 +471,14 @@ msgstr "レイアウト"
|
||||
msgid "Light"
|
||||
msgstr "ライト"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "ログアウト"
|
||||
@@ -734,6 +760,7 @@ msgstr "スワップ使用量"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "システム"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "テーマを切り替え"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "トークン"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "トークンはエージェントの接続と登録を可能にします
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "センサーがしきい値を超えたときにトリガーされます"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "有効にすると、このトークンはエージェントが事前のシステム作成なしに自己登録することを可能にします。1時間後またはハブの再起動時に期限切れになります。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows コマンド"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "클립보드에 복사됨"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "docker compose 복사"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "docker run 복사"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "호스트 복사"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "리눅스 명령어 복사"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "알림 수정 실패"
|
||||
msgid "Filter..."
|
||||
msgstr "필터..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {분} other {분}} 동안"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "그리드"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew 명령어"
|
||||
@@ -440,6 +448,16 @@ msgstr "잘못된 이메일 주소입니다."
|
||||
msgid "Kernel"
|
||||
msgstr "커널"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "언어"
|
||||
@@ -453,6 +471,14 @@ msgstr "레이아웃"
|
||||
msgid "Light"
|
||||
msgstr "밝게"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "로그아웃"
|
||||
@@ -734,6 +760,7 @@ msgstr "스왑 사용량"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "시스템"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "테마 전환"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "센서가 임계값을 초과할 때 트리거됩니다."
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows 명령어"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Gekopieerd naar het klembord"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopiëren"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopiëren"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopieer host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopieer Linux-opdracht"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Bijwerken waarschuwing mislukt"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Voor <0>{min}</0> {min, plural, one {minuut} other {minuten}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Raster"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-commando"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ongeldig e-mailadres."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Taal"
|
||||
@@ -453,6 +471,14 @@ msgstr "Indeling"
|
||||
msgid "Light"
|
||||
msgstr "Licht"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Afmelden"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap gebruik"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Systeem"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Schakel thema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggert wanneer een sensor een drempelwaarde overschrijdt"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-commando"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopiert til utklippstavlen"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopier docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopier docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopier vert"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopier Linux-kommando"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Kunne ikke oppdatere alarm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "I <0>{min}</0> {min, plural, one {minutt} other {minutter}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rutenett"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ugyldig e-postadresse."
|
||||
msgid "Kernel"
|
||||
msgstr "Kjerne"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Språk"
|
||||
@@ -453,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Lyst"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Logg Ut"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap-bruk"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Tema av/på"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Slår inn når enhver sensor overstiger en grenseverdi"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Skopiowano do schowka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Skopiuj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Skopiuj docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopiuj host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiuj polecenie Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Nie udało się zaktualizować powiadomienia"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtruj..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Na <0>{min}</0> {min, plural, one {minutę} other {minut}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Siatka"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Polecenie Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Nieprawidłowy adres e-mail."
|
||||
msgid "Kernel"
|
||||
msgstr "Jądro"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Język"
|
||||
@@ -453,6 +471,14 @@ msgstr "Układ"
|
||||
msgid "Light"
|
||||
msgstr "Jasny"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Wyloguj"
|
||||
@@ -734,6 +760,7 @@ msgstr "Użycie pamięci wymiany"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Zmień motyw"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg."
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Polecenie Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Copiado para a área de transferência"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Copiar docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Copiar docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Copiar host"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copiar comando Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Falha ao atualizar alerta"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrar..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Grade"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Comando Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Endereço de email inválido."
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
@@ -453,6 +471,14 @@ msgstr "Aspeto"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Sair"
|
||||
@@ -734,6 +760,7 @@ msgstr "Uso de Swap"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Alternar tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Os tokens permitem que os agentes se conectem e registrem. As impressõe
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Dispara quando qualquer sensor excede um limite"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Quando habilitado, este token permite que os agentes se registrem automaticamente sem criação prévia do sistema. Expira após uma hora ou na reinicialização do hub."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Comando Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Скопировано в буфер обмена"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Скопировать docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Скопировать docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Копировать хост"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Копировать команду Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Не удалось обновить оповещение"
|
||||
msgid "Filter..."
|
||||
msgstr "Фильтр..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "На <0>{min}</0> {min, plural, one {минуту} other {минут}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Сетка"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Команда Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Неверный адрес электронной почты."
|
||||
msgid "Kernel"
|
||||
msgstr "Ядро"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Язык"
|
||||
@@ -453,6 +471,14 @@ msgstr "Макет"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Выйти"
|
||||
@@ -734,6 +760,7 @@ msgstr "Использование подкачки"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Переключить тему"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Срабатывает, когда любой датчик превышает порог"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Команда Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopirano v odložišče"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiraj docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiraj docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopiraj gostitelja"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiraj Linux ukaz"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Opozorila ni bilo mogoče posodobiti"
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Za <0>{min}</0> {min, plural, one {minuto} other {minut}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Mreža"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Ukaz Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Napačen e-poštni naslov."
|
||||
msgid "Kernel"
|
||||
msgstr "Jedro"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jezik"
|
||||
@@ -453,6 +471,14 @@ msgstr "Postavitev"
|
||||
msgid "Light"
|
||||
msgstr "Svetlo"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Odjava"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap uporaba"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistemsko"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Obrni temo"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Sproži se, ko kateri koli senzor preseže prag"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Ukaz Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Kopierat till urklipp"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Kopiera docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Kopiera docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Kopiera värd"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Kopiera Linux-kommando"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Kunde inte uppdatera larm"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrera..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Under <0>{min}</0> {min, plural, one {minut} other {minuter}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Rutnät"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew-kommando"
|
||||
@@ -440,6 +448,16 @@ msgstr "Ogiltig e-postadress."
|
||||
msgid "Kernel"
|
||||
msgstr "Kärna"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Språk"
|
||||
@@ -453,6 +471,14 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Ljust"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Logga ut"
|
||||
@@ -734,6 +760,7 @@ msgstr "Swap-användning"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Växla tema"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Utlöses när någon sensor överskrider ett tröskelvärde"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows-kommando"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Panoya kopyalandı"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Docker compose kopyala"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Docker run kopyala"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Ana bilgisayarı kopyala"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Linux komutunu kopyala"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Uyarı güncellenemedi"
|
||||
msgid "Filter..."
|
||||
msgstr "Filtrele..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "<0>{min}</0> {min, plural, one {dakika} other {dakika}} için"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Izgara"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew komutu"
|
||||
@@ -440,6 +448,16 @@ msgstr "Geçersiz e-posta adresi."
|
||||
msgid "Kernel"
|
||||
msgstr "Çekirdek"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Dil"
|
||||
@@ -453,6 +471,14 @@ msgstr "Düzen"
|
||||
msgid "Light"
|
||||
msgstr "Açık"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Çıkış Yap"
|
||||
@@ -734,6 +760,7 @@ msgstr "Takas Kullanımı"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Sistem"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Temayı değiştir"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Token'lar agentların bağlanıp kaydolmasına izin verir. Parmak izleri
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Token'lar ve parmak izleri hub'a WebSocket bağlantılarını doğrulamak için kullanılır."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Herhangi bir sensör bir eşiği aştığında tetiklenir"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Etkinleştirildiğinde, bu token agentların önceden sistem oluşturmadan kendilerini kaydetmelerine izin verir. Bir saat sonra veya hub yeniden başlatıldığında sona erer."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows komutu"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Скопійовано в буфер обміну"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Копіювати docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Копіювати docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Копіювати хост"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Копіювати команду Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Не вдалося оновити сповіщення"
|
||||
msgid "Filter..."
|
||||
msgstr "Фільтр..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Протягом <0>{min}</0> {min, plural, one {хвилини} other {хвилин}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Сітка"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Команда Homebrew"
|
||||
@@ -440,6 +448,16 @@ msgstr "Неправильна адреса електронної пошти."
|
||||
msgid "Kernel"
|
||||
msgstr "Ядро"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Мова"
|
||||
@@ -453,6 +471,14 @@ msgstr "Макет"
|
||||
msgid "Light"
|
||||
msgstr "Світлий"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Вийти"
|
||||
@@ -734,6 +760,7 @@ msgstr "Використання підкачки"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Система"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Перемкнути тему"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "Токен"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "Токени дозволяють агентам підключатис
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "Токени та відбитки використовуються для автентифікації WebSocket з'єднань до хабу."
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Спрацьовує, коли будь-який датчик перевищує поріг"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "Коли увімкнено, цей токен дозволяє агентам самостійно реєструватися без попереднього створення системи. Термін дії закінчується через годину або при перезапуску хабу."
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Команда Windows"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "Đã sao chép vào clipboard"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "Sao chép docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "Sao chép docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "Sao chép máy chủ"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Sao chép lệnh Linux"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "Cập nhật cảnh báo thất bại"
|
||||
msgid "Filter..."
|
||||
msgstr "Lọc..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "Trong <0>{min}</0> {min, plural, one {phút} other {phút}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "Lưới"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr ""
|
||||
@@ -440,6 +448,16 @@ msgstr "Địa chỉ email không hợp lệ."
|
||||
msgid "Kernel"
|
||||
msgstr "Nhân"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "Ngôn ngữ"
|
||||
@@ -453,6 +471,14 @@ msgstr "Bố cục"
|
||||
msgid "Light"
|
||||
msgstr "Sáng"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "Đăng xuất"
|
||||
@@ -734,6 +760,7 @@ msgstr "Sử dụng Hoán đổi"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "Hệ thống"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "Chuyển đổi chủ đề"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr ""
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Kích hoạt khi bất kỳ cảm biến nào vượt quá ngưỡng"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr ""
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr ""
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "已复制到剪贴板"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "复制 docker compose 文件"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "复制 docker run 命令"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "复制主机名"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "复制 Linux 安装命令"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "更新警报失败"
|
||||
msgid "Filter..."
|
||||
msgstr "过滤..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "持续<0>{min}</0> {min, plural, one {分钟} other {分钟}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "网格"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew 安装命令"
|
||||
@@ -440,6 +448,16 @@ msgstr "无效的电子邮件地址。"
|
||||
msgid "Kernel"
|
||||
msgstr "内核"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "语言"
|
||||
@@ -453,6 +471,14 @@ msgstr "布局"
|
||||
msgid "Light"
|
||||
msgstr "浅色模式"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "登出"
|
||||
@@ -734,6 +760,7 @@ msgstr "SWAP 使用"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "系统"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "切换主题"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "令牌"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "令牌允许客户端连接和注册。指纹是每个系统唯一的稳
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "令牌和指纹用于验证到中心的WebSocket连接。"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "当任何传感器超过阈值时触发"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "启用时,此令牌允许客户端在无需预先创建系统的情况下自动注册。在一小时后或中心重启时过期。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows 安装命令"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "已複製到剪貼板"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "複製 docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "複製 docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "複製主機"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "複製 Linux 指令"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "更新警報失敗"
|
||||
msgid "Filter..."
|
||||
msgstr "篩選..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "網格"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew 指令"
|
||||
@@ -440,6 +448,16 @@ msgstr "無效的電子郵件地址。"
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "語言"
|
||||
@@ -453,6 +471,14 @@ msgstr "版面配置"
|
||||
msgid "Light"
|
||||
msgstr "淺色"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "登出"
|
||||
@@ -734,6 +760,7 @@ msgstr "交換使用"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "系統"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "切換主題"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "令牌"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "令牌允許代理程式連接和註冊。指紋是每個系統唯一的
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "令牌和指紋用於驗證到中心的WebSocket連接。"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "當任何傳感器超過閾值時觸發"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "啟用時,此令牌允許代理程式在無需預先創建系統的情況下自動註冊。在一小時後或中心重啟時過期。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows 指令"
|
||||
|
||||
@@ -211,11 +211,13 @@ msgid "Copied to clipboard"
|
||||
msgstr "已複製到剪貼簿"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker compose file content"
|
||||
msgid "Copy docker compose"
|
||||
msgstr "复制 docker compose"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy docker run command"
|
||||
msgid "Copy docker run"
|
||||
msgstr "复制 docker run"
|
||||
@@ -230,6 +232,7 @@ msgid "Copy host"
|
||||
msgstr "複製主機"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy Linux command"
|
||||
msgstr "複製 Linux 指令"
|
||||
|
||||
@@ -396,6 +399,10 @@ msgstr "更新警報失敗"
|
||||
msgid "Filter..."
|
||||
msgstr "篩選..."
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Fingerprint"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}"
|
||||
@@ -419,6 +426,7 @@ msgid "Grid"
|
||||
msgstr "網格"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Homebrew command"
|
||||
msgstr "Homebrew 命令"
|
||||
@@ -440,6 +448,16 @@ msgstr "無效的電子郵件地址。"
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
#. Load average 15 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L15"
|
||||
msgstr ""
|
||||
|
||||
#. Load average 5 minutes
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "L5"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Language"
|
||||
msgstr "語言"
|
||||
@@ -453,6 +471,14 @@ msgstr "版面配置"
|
||||
msgid "Light"
|
||||
msgstr "淺色"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 15m"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Load Average 5m"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Log Out"
|
||||
msgstr "登出"
|
||||
@@ -734,6 +760,7 @@ msgstr "虛擬記憶體使用量"
|
||||
#: src/lib/utils.ts
|
||||
#: src/components/mode-toggle.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "System"
|
||||
msgstr "系統"
|
||||
|
||||
@@ -801,6 +828,7 @@ msgid "Toggle theme"
|
||||
msgstr "切換主題"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Token"
|
||||
msgstr "令牌"
|
||||
|
||||
@@ -818,6 +846,14 @@ msgstr "令牌允許代理程式連接和註冊。指紋是每個系統唯一的
|
||||
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
|
||||
msgstr "令牌和指紋用於驗證到中心的WebSocket連接。"
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 15 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when 5 minute load average exceeds a threshold"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/utils.ts
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "當任何感應器超過閾值時觸發"
|
||||
@@ -906,6 +942,7 @@ msgid "When enabled, this token allows agents to self-register without prior sys
|
||||
msgstr "啟用時,此令牌允許代理程式在無需預先創建系統的情況下自動註冊。在一小時後或中心重啟時過期。"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgctxt "Button to copy install command"
|
||||
msgid "Windows command"
|
||||
msgstr "Windows 命令"
|
||||
|
||||
32
beszel/site/src/types.d.ts
vendored
32
beszel/site/src/types.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { RecordModel } from "pocketbase"
|
||||
import { Os } from "./lib/enums"
|
||||
import { Unit, Os } from "./lib/enums"
|
||||
|
||||
// global window properties
|
||||
declare global {
|
||||
@@ -44,6 +44,12 @@ export interface SystemInfo {
|
||||
c: number
|
||||
/** cpu model */
|
||||
m: string
|
||||
/** load average 1 minute */
|
||||
l1?: number
|
||||
/** load average 5 minutes */
|
||||
l5?: number
|
||||
/** load average 15 minutes */
|
||||
l15?: number
|
||||
/** operating system */
|
||||
o?: string
|
||||
/** uptime */
|
||||
@@ -71,6 +77,12 @@ export interface SystemStats {
|
||||
cpu: number
|
||||
/** peak cpu */
|
||||
cpum?: number
|
||||
/** load average 1 minute */
|
||||
l1?: number
|
||||
/** load average 5 minutes */
|
||||
l5?: number
|
||||
/** load average 15 minutes */
|
||||
l15?: number
|
||||
/** total memory (gb) */
|
||||
m: number
|
||||
/** memory used (gb) */
|
||||
@@ -177,6 +189,16 @@ export interface AlertRecord extends RecordModel {
|
||||
// user: string
|
||||
}
|
||||
|
||||
export interface AlertsHistoryRecord extends RecordModel {
|
||||
alert: string
|
||||
user: string
|
||||
system: string
|
||||
name: string
|
||||
val: number
|
||||
created: string
|
||||
resolved?: string | null
|
||||
}
|
||||
|
||||
export type ChartTimes = "1h" | "12h" | "24h" | "1w" | "30d"
|
||||
|
||||
export interface ChartTimeData {
|
||||
@@ -190,11 +212,14 @@ export interface ChartTimeData {
|
||||
}
|
||||
}
|
||||
|
||||
export type UserSettings = {
|
||||
export interface UserSettings {
|
||||
// lang?: string
|
||||
chartTime: ChartTimes
|
||||
emails?: string[]
|
||||
webhooks?: string[]
|
||||
unitTemp?: Unit
|
||||
unitNet?: Unit
|
||||
unitDisk?: Unit
|
||||
}
|
||||
|
||||
type ChartDataContainer = {
|
||||
@@ -218,6 +243,9 @@ interface AlertInfo {
|
||||
icon: any
|
||||
desc: () => string
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
start?: number
|
||||
/** Single value description (when there's only one value, like status) */
|
||||
singleDesc?: () => string
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = {
|
||||
center: true,
|
||||
padding: "1rem",
|
||||
screens: {
|
||||
"2xl": "1420px",
|
||||
"2xl": "1440px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
@@ -91,6 +91,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/container-queries"),
|
||||
require("tailwindcss-animate"),
|
||||
require("tailwindcss-rtl"),
|
||||
function ({ addVariant }) {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2021",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
@@ -3,8 +3,8 @@ package beszel
|
||||
import "github.com/blang/semver"
|
||||
|
||||
const (
|
||||
Version = "0.12.0-beta1"
|
||||
Version = "0.12.0-beta2"
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
var MinVersionCbor = semver.MustParse("0.12.0-beta1")
|
||||
var MinVersionCbor = semver.MustParse("0.12.0-beta2")
|
||||
|
||||
@@ -144,7 +144,7 @@ while [ $# -gt 0 ]; do
|
||||
shift
|
||||
HUB_URL="$1"
|
||||
;;
|
||||
-v|--version)
|
||||
-v | --version)
|
||||
shift
|
||||
VERSION="$1"
|
||||
;;
|
||||
@@ -353,8 +353,8 @@ if is_alpine; then
|
||||
fi
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
if getent group docker; then
|
||||
echo "Adding besel to docker group"
|
||||
usermod -aG docker beszel
|
||||
echo "Adding beszel to docker group"
|
||||
usermod -aG docker beszel
|
||||
fi
|
||||
|
||||
else
|
||||
@@ -362,10 +362,10 @@ else
|
||||
echo "Creating a dedicated user for the Beszel Agent service..."
|
||||
useradd --system --home-dir /nonexistent --shell /bin/false beszel
|
||||
fi
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
if getent group docker; then
|
||||
echo "Adding besel to docker group"
|
||||
usermod -aG docker beszel
|
||||
echo "Adding beszel to docker group"
|
||||
usermod -aG docker beszel
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -323,8 +323,8 @@ if is_alpine; then
|
||||
fi
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
if getent group docker; then
|
||||
echo "Adding besel to docker group"
|
||||
usermod -aG docker beszel
|
||||
echo "Adding beszel to docker group"
|
||||
usermod -aG docker beszel
|
||||
fi
|
||||
|
||||
else
|
||||
@@ -332,10 +332,10 @@ else
|
||||
echo "Creating a dedicated user for the Beszel Agent service..."
|
||||
useradd --system --home-dir /nonexistent --shell /bin/false beszel
|
||||
fi
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
# Add the user to the docker group to allow access to the Docker socket if group docker exists
|
||||
if getent group docker; then
|
||||
echo "Adding besel to docker group"
|
||||
usermod -aG docker beszel
|
||||
echo "Adding beszel to docker group"
|
||||
usermod -aG docker beszel
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user