Compare commits

...

11 Commits

Author SHA1 Message Date
henrygd
02d594cc82 release 0.15.4 2025-11-04 17:23:42 -05:00
henrygd
7d0b5c1c67 update language files 2025-11-04 17:18:57 -05:00
Thiago Alves Cavalcante
d3dc8a7af0 new Portuguese translations 2025-11-04 17:18:07 -05:00
henrygd
d67fefe7c5 new spanish translations by dtornerte 2025-11-04 17:17:02 -05:00
henrygd
4d364c5e4d update language files 2025-11-04 17:06:51 -05:00
henrygd
954400ea45 fix intel_gpu_top parsing when engine instance id is in column (#1230) 2025-11-04 16:02:20 -05:00
henrygd
04b6067e64 add a total line to the tooltip of charts with multiple values #1280
Co-authored-by: Titouan V <titouan.verhille@gmail.com>
2025-11-04 15:41:24 -05:00
henrygd
d77ee5554f add fallback paths for smartctl lookup (#1362, #1363) 2025-11-04 14:06:28 -05:00
henrygd
2e034bdead refactor containers table to fix clock issue causing no results (#1337) 2025-11-04 13:18:34 -05:00
henrygd
fc0947aa04 fix windows extra disk backslash issue (#1361) 2025-11-03 17:42:08 -05:00
henrygd
1d546a4091 update nvidia dockerfile to build latest smartmontools (#1335) 2025-11-02 17:13:47 -05:00
43 changed files with 567 additions and 226 deletions

View File

@@ -31,6 +31,7 @@ func (a *Agent) initializeDiskInfo() {
filesystem, _ := GetEnv("FILESYSTEM") filesystem, _ := GetEnv("FILESYSTEM")
efPath := "/extra-filesystems" efPath := "/extra-filesystems"
hasRoot := false hasRoot := false
isWindows := runtime.GOOS == "windows"
partitions, err := disk.Partitions(false) partitions, err := disk.Partitions(false)
if err != nil { if err != nil {
@@ -38,6 +39,13 @@ func (a *Agent) initializeDiskInfo() {
} }
slog.Debug("Disk", "partitions", partitions) slog.Debug("Disk", "partitions", partitions)
// trim trailing backslash for Windows devices (#1361)
if isWindows {
for i, p := range partitions {
partitions[i].Device = strings.TrimSuffix(p.Device, "\\")
}
}
// ioContext := context.WithValue(a.sensorsContext, // ioContext := context.WithValue(a.sensorsContext,
// common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/tmp/testproc"}, // common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/tmp/testproc"},
// ) // )
@@ -52,7 +60,7 @@ func (a *Agent) initializeDiskInfo() {
// Helper function to add a filesystem to fsStats if it doesn't exist // Helper function to add a filesystem to fsStats if it doesn't exist
addFsStat := func(device, mountpoint string, root bool, customName ...string) { addFsStat := func(device, mountpoint string, root bool, customName ...string) {
var key string var key string
if runtime.GOOS == "windows" { if isWindows {
key = device key = device
} else { } else {
key = filepath.Base(device) key = filepath.Base(device)

View File

@@ -134,7 +134,9 @@ func (gm *GPUManager) parseIntelHeaders(header1 string, header2 string) (engineN
powerIndex = -1 // Initialize to -1, will be set to actual index if found powerIndex = -1 // Initialize to -1, will be set to actual index if found
// Collect engine names from header1 // Collect engine names from header1
for _, col := range h1 { for _, col := range h1 {
key := strings.TrimRightFunc(col, func(r rune) bool { return r >= '0' && r <= '9' }) key := strings.TrimRightFunc(col, func(r rune) bool {
return (r >= '0' && r <= '9') || r == '/'
})
var friendly string var friendly string
switch key { switch key {
case "RCS": case "RCS":

View File

@@ -1439,6 +1439,15 @@ func TestParseIntelHeaders(t *testing.T) {
wantPowerIndex: 4, // "gpu" is at index 4 wantPowerIndex: 4, // "gpu" is at index 4
wantPreEngineCols: 8, // 17 total cols - 3*3 = 8 wantPreEngineCols: 8, // 17 total cols - 3*3 = 8
}, },
{
name: "basic headers with RCS BCS VCS using index in name",
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS/0 BCS/1 VCS/2",
header2: " req act /s % gpu pkg rd wr % se wa % se wa % se wa",
wantEngineNames: []string{"RCS", "BCS", "VCS"},
wantFriendlyNames: []string{"Render/3D", "Blitter", "Video"},
wantPowerIndex: 4, // "gpu" is at index 4
wantPreEngineCols: 8, // 17 total cols - 3*3 = 8
},
{ {
name: "headers with only RCS", name: "headers with only RCS",
header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS", header1: "Freq MHz IRQ RC6 Power W IMC MiB/s RCS",

View File

@@ -5,7 +5,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -23,6 +25,7 @@ type SmartManager struct {
SmartDevices []*DeviceInfo SmartDevices []*DeviceInfo
refreshMutex sync.Mutex refreshMutex sync.Mutex
lastScanTime time.Time lastScanTime time.Time
binPath string
} }
type scanOutput struct { type scanOutput struct {
@@ -160,7 +163,7 @@ func (sm *SmartManager) ScanDevices(force bool) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, "smartctl", "--scan", "-j") cmd := exec.CommandContext(ctx, sm.binPath, "--scan", "-j")
output, err := cmd.Output() output, err := cmd.Output()
var ( var (
@@ -382,7 +385,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
// Try with -n standby first if we have existing data // Try with -n standby first if we have existing data
args := sm.smartctlArgs(deviceInfo, true) args := sm.smartctlArgs(deviceInfo, true)
cmd := exec.CommandContext(ctx, "smartctl", args...) cmd := exec.CommandContext(ctx, sm.binPath, args...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
// Check if device is in standby (exit status 2) // Check if device is in standby (exit status 2)
@@ -395,7 +398,7 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second) ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel2() defer cancel2()
args = sm.smartctlArgs(deviceInfo, false) args = sm.smartctlArgs(deviceInfo, false)
cmd = exec.CommandContext(ctx2, "smartctl", args...) cmd = exec.CommandContext(ctx2, sm.binPath, args...)
output, err = cmd.CombinedOutput() output, err = cmd.CombinedOutput()
} }
@@ -875,13 +878,24 @@ func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
} }
// detectSmartctl checks if smartctl is installed, returns an error if not // detectSmartctl checks if smartctl is installed, returns an error if not
func (sm *SmartManager) detectSmartctl() error { func (sm *SmartManager) detectSmartctl() (string, error) {
if _, err := exec.LookPath("smartctl"); err == nil { if path, err := exec.LookPath("smartctl"); err == nil {
slog.Debug("smartctl found") return path, nil
return nil
} }
slog.Debug("smartctl not found") locations := []string{}
return errors.New("smartctl not found") if runtime.GOOS == "windows" {
locations = append(locations,
"C:\\Program Files\\smartmontools\\bin\\smartctl.exe",
)
} else {
locations = append(locations, "/opt/homebrew/bin/smartctl")
}
for _, location := range locations {
if _, err := os.Stat(location); err == nil {
return location, nil
}
}
return "", errors.New("smartctl not found")
} }
// NewSmartManager creates and initializes a new SmartManager // NewSmartManager creates and initializes a new SmartManager
@@ -889,9 +903,12 @@ func NewSmartManager() (*SmartManager, error) {
sm := &SmartManager{ sm := &SmartManager{
SmartDataMap: make(map[string]*smart.SmartData), SmartDataMap: make(map[string]*smart.SmartData),
} }
if err := sm.detectSmartctl(); err != nil { path, err := sm.detectSmartctl()
if err != nil {
slog.Debug(err.Error())
return nil, err return nil, err
} }
slog.Debug("smartctl", "path", path)
sm.binPath = path
return sm, nil return sm, nil
} }

View File

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

View File

@@ -2,7 +2,6 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app WORKDIR /app
COPY ../go.mod ../go.sum ./ COPY ../go.mod ../go.sum ./
RUN go mod download RUN go mod download
@@ -13,7 +12,24 @@ COPY . ./
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
RUN rm -rf /tmp/* # --------------------------
# Smartmontools builder stage
# --------------------------
FROM nvidia/cuda:12.2.2-base-ubuntu22.04 AS smartmontools-builder
RUN apt-get update && apt-get install -y \
wget \
build-essential \
&& wget https://downloads.sourceforge.net/project/smartmontools/smartmontools/7.5/smartmontools-7.5.tar.gz \
&& tar zxvf smartmontools-7.5.tar.gz \
&& cd smartmontools-7.5 \
&& ./configure --prefix=/usr --sysconfdir=/etc \
&& make \
&& make install \
&& rm -rf /smartmontools-7.5* \
&& apt-get remove -y wget build-essential \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
# -------------------------- # --------------------------
# Final image: GPU-enabled agent with nvidia-smi # Final image: GPU-enabled agent with nvidia-smi
@@ -21,10 +37,8 @@ RUN rm -rf /tmp/*
FROM nvidia/cuda:12.2.2-base-ubuntu22.04 FROM nvidia/cuda:12.2.2-base-ubuntu22.04
COPY --from=builder /agent /agent COPY --from=builder /agent /agent
# this is so we don't need to create the /tmp directory in the scratch container # Copy smartmontools binaries and config files
COPY --from=builder /tmp /tmp COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
RUN apt-get update && apt-get install -y smartmontools && rm -rf /var/lib/apt/lists/*
# Ensure data persistence across container recreations # Ensure data persistence across container recreations
VOLUME ["/var/lib/beszel-agent"] VOLUME ["/var/lib/beszel-agent"]

View File

@@ -1,12 +1,12 @@
{ {
"name": "beszel", "name": "beszel",
"version": "0.15.3", "version": "0.15.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "beszel", "name": "beszel",
"version": "0.15.3", "version": "0.15.4",
"dependencies": { "dependencies": {
"@henrygd/queue": "^1.0.7", "@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2", "@henrygd/semaphore": "^0.0.2",

View File

@@ -1,7 +1,7 @@
{ {
"name": "beszel", "name": "beszel",
"private": true, "private": true,
"version": "0.15.3", "version": "0.15.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",

View File

@@ -30,6 +30,7 @@ export default function AreaChartDefault({
domain, domain,
legend, legend,
itemSorter, itemSorter,
showTotal = false,
reverseStackOrder = false, reverseStackOrder = false,
hideYAxis = false, hideYAxis = false,
}: // logRender = false, }: // logRender = false,
@@ -42,6 +43,7 @@ export default function AreaChartDefault({
dataPoints?: DataPoint[] dataPoints?: DataPoint[]
domain?: [number, number] domain?: [number, number]
legend?: boolean legend?: boolean
showTotal?: boolean
itemSorter?: (a: any, b: any) => number itemSorter?: (a: any, b: any) => number
reverseStackOrder?: boolean reverseStackOrder?: boolean
hideYAxis?: boolean hideYAxis?: boolean
@@ -65,18 +67,25 @@ export default function AreaChartDefault({
"ps-4": hideYAxis, "ps-4": hideYAxis,
})} })}
> >
<AreaChart reverseStackOrder={reverseStackOrder} accessibilityLayer data={chartData.systemStats} margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}> <AreaChart
reverseStackOrder={reverseStackOrder}
accessibilityLayer
data={chartData.systemStats}
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
{!hideYAxis && <YAxis {!hideYAxis && (
direction="ltr" <YAxis
orientation={chartData.orientation} direction="ltr"
className="tracking-tighter" orientation={chartData.orientation}
width={yAxisWidth} className="tracking-tighter"
domain={domain ?? [0, max ?? "auto"]} width={yAxisWidth}
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))} domain={domain ?? [0, max ?? "auto"]}
tickLine={false} tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
axisLine={false} tickLine={false}
/>} axisLine={false}
/>
)}
{xAxis(chartData)} {xAxis(chartData)}
<ChartTooltip <ChartTooltip
animationEasing="ease-out" animationEasing="ease-out"
@@ -87,6 +96,7 @@ export default function AreaChartDefault({
<ChartTooltipContent <ChartTooltipContent
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
contentFormatter={contentFormatter} contentFormatter={contentFormatter}
showTotal={showTotal}
/> />
} }
/> />
@@ -114,5 +124,5 @@ export default function AreaChartDefault({
</ChartContainer> </ChartContainer>
</div> </div>
) )
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled]) }, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal])
} }

View File

@@ -139,7 +139,7 @@ export default memo(function ContainerChart({
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)} labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
// @ts-expect-error // @ts-expect-error
itemSorter={(a, b) => b.value - a.value} itemSorter={(a, b) => b.value - a.value}
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />} content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} showTotal={true} />}
/> />
{Object.keys(chartConfig).map((key) => { {Object.keys(chartConfig).map((key) => {
const filtered = filteredKeys.has(key) const filtered = filteredKeys.has(key)

View File

@@ -61,6 +61,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true) const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
}} }}
showTotal={true}
/> />
} }
/> />

View File

@@ -35,6 +35,7 @@ import { getPagePath } from "@nanostores/router"
const syntaxTheme = "github-dark-dimmed" const syntaxTheme = "github-dark-dimmed"
export default function ContainersTable({ systemId }: { systemId?: string }) { export default function ContainersTable({ systemId }: { systemId?: string }) {
const loadTime = Date.now()
const [data, setData] = useState<ContainerRecord[]>([]) const [data, setData] = useState<ContainerRecord[]>([])
const [sorting, setSorting] = useBrowserStorage<SortingState>( const [sorting, setSorting] = useBrowserStorage<SortingState>(
`sort-c-${systemId ? 1 : 0}`, `sort-c-${systemId ? 1 : 0}`,
@@ -47,56 +48,53 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
const [globalFilter, setGlobalFilter] = useState("") const [globalFilter, setGlobalFilter] = useState("")
useEffect(() => { useEffect(() => {
const pbOptions = { function fetchData(systemId?: string) {
fields: "id,name,image,cpu,memory,net,health,status,system,updated",
}
const fetchData = (lastXMs: number) => {
const updated = Date.now() - lastXMs
let filter: string
if (systemId) {
filter = pb.filter("system={:system} && updated > {:updated}", { system: systemId, updated })
} else {
filter = pb.filter("updated > {:updated}", { updated })
}
pb.collection<ContainerRecord>("containers") pb.collection<ContainerRecord>("containers")
.getList(0, 2000, { .getList(0, 2000, {
...pbOptions, fields: "id,name,image,cpu,memory,net,health,status,system,updated",
filter, filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
}) })
.then(({ items }) => setData((curItems) => { .then(({ items }) => items.length && setData((curItems) => {
const containerIds = new Set(items.map(item => item.id)) const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const now = Date.now() const containerIds = new Set()
for (const item of curItems) { const newItems = []
if (!containerIds.has(item.id) && now - item.updated < 70_000) { for (const item of items) {
items.push(item) if (Math.abs(lastUpdated - item.updated) < 70_000) {
containerIds.add(item.id)
newItems.push(item)
} }
} }
return items for (const item of curItems) {
if (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {
newItems.push(item)
}
}
return newItems
})) }))
} }
// initial load // initial load
fetchData(70_000) fetchData(systemId)
// if no systemId, poll every 10 seconds // if no systemId, pull system containers after every system update
if (!systemId) { if (!systemId) {
// poll every 10 seconds return $allSystemsById.listen((_value, _oldValue, systemId) => {
const intervalId = setInterval(() => fetchData(10_500), 10_000) // exclude initial load of systems
// clear interval on unmount if (Date.now() - loadTime > 500) {
return () => clearInterval(intervalId) fetchData(systemId)
}
})
} }
// if systemId, fetch containers after the system is updated // if systemId, fetch containers after the system is updated
return listenKeys($allSystemsById, [systemId], (_newSystems) => { return listenKeys($allSystemsById, [systemId], (_newSystems) => {
const changeTime = Date.now() fetchData(systemId)
setTimeout(() => fetchData(Date.now() - changeTime + 1000), 100)
}) })
}, []) }, [])
const table = useReactTable({ const table = useReactTable({
data, data,
columns: containerChartCols.filter(col => systemId ? col.id !== "system" : true), columns: containerChartCols.filter((col) => (systemId ? col.id !== "system" : true)),
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
@@ -164,77 +162,78 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
) )
} }
const AllContainersTable = memo( const AllContainersTable = memo(function AllContainersTable({
function AllContainersTable({ table, rows, colLength }: { table: TableType<ContainerRecord>; rows: Row<ContainerRecord>[]; colLength: number }) { table,
// The virtualizer will need a reference to the scrollable container element rows,
const scrollRef = useRef<HTMLDivElement>(null) colLength,
const activeContainer = useRef<ContainerRecord | null>(null) }: {
const [sheetOpen, setSheetOpen] = useState(false) table: TableType<ContainerRecord>
const openSheet = (container: ContainerRecord) => { rows: Row<ContainerRecord>[]
activeContainer.current = container colLength: number
setSheetOpen(true) }) {
} // The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({ const activeContainer = useRef<ContainerRecord | null>(null)
count: rows.length, const [sheetOpen, setSheetOpen] = useState(false)
estimateSize: () => 54, const openSheet = (container: ContainerRecord) => {
getScrollElement: () => scrollRef.current, activeContainer.current = container
overscan: 5, setSheetOpen(true)
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full text-nowrap">
<ContainersTableHead table={table} />
<TableBody>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<ContainerTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
openSheet={openSheet}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No results.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
<ContainerSheet sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} activeContainer={activeContainer} />
</div>
)
} }
)
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => 54,
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full text-nowrap">
<ContainersTableHead table={table} />
<TableBody>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
return <ContainerTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No results.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
<ContainerSheet sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} activeContainer={activeContainer} />
</div>
)
})
async function getLogsHtml(container: ContainerRecord): Promise<string> { async function getLogsHtml(container: ContainerRecord): Promise<string> {
try { try {
const [{ highlighter }, logsHtml] = await Promise.all([import('@/lib/shiki'), pb.send<{ logs: string }>("/api/beszel/containers/logs", { const [{ highlighter }, logsHtml] = await Promise.all([
system: container.system, import("@/lib/shiki"),
container: container.id, pb.send<{ logs: string }>("/api/beszel/containers/logs", {
})]) system: container.system,
container: container.id,
}),
])
return logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: "log", theme: syntaxTheme }) : t`No results.` return logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: "log", theme: syntaxTheme }) : t`No results.`
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -244,10 +243,13 @@ async function getLogsHtml(container: ContainerRecord): Promise<string> {
async function getInfoHtml(container: ContainerRecord): Promise<string> { async function getInfoHtml(container: ContainerRecord): Promise<string> {
try { try {
let [{ highlighter }, { info }] = await Promise.all([import('@/lib/shiki'), pb.send<{ info: string }>("/api/beszel/containers/info", { let [{ highlighter }, { info }] = await Promise.all([
system: container.system, import("@/lib/shiki"),
container: container.id, pb.send<{ info: string }>("/api/beszel/containers/info", {
})]) system: container.system,
container: container.id,
}),
])
try { try {
info = JSON.stringify(JSON.parse(info), null, 2) info = JSON.stringify(JSON.parse(info), null, 2)
} catch (_) { } } catch (_) { }
@@ -258,7 +260,15 @@ async function getInfoHtml(container: ContainerRecord): Promise<string> {
} }
} }
function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpen: boolean, setSheetOpen: (open: boolean) => void, activeContainer: RefObject<ContainerRecord | null> }) { function ContainerSheet({
sheetOpen,
setSheetOpen,
activeContainer,
}: {
sheetOpen: boolean
setSheetOpen: (open: boolean) => void
activeContainer: RefObject<ContainerRecord | null>
}) {
const container = activeContainer.current const container = activeContainer.current
if (!container) return null if (!container) return null
@@ -297,14 +307,14 @@ function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpe
useEffect(() => { useEffect(() => {
setLogsDisplay("") setLogsDisplay("")
setInfoDisplay(""); setInfoDisplay("")
if (!container) return if (!container) return
(async () => { ; (async () => {
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)]) const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
setLogsDisplay(logsHtml) setLogsDisplay(logsHtml)
setInfoDisplay(infoHtml) setInfoDisplay(infoHtml)
setTimeout(scrollLogsToBottom, 20) setTimeout(scrollLogsToBottom, 20)
})() })()
}, [container]) }, [container])
return ( return (
@@ -328,7 +338,9 @@ function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpe
<SheetHeader> <SheetHeader>
<SheetTitle>{container.name}</SheetTitle> <SheetTitle>{container.name}</SheetTitle>
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1"> <SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
<Link className="hover:underline" href={getPagePath($router, "system", { id: container.system })}>{$allSystemsById.get()[container.system]?.name ?? ""}</Link> <Link className="hover:underline" href={getPagePath($router, "system", { id: container.system })}>
{$allSystemsById.get()[container.system]?.name ?? ""}
</Link>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" /> <Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
{container.status} {container.status}
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" /> <Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
@@ -350,19 +362,20 @@ function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpe
disabled={isRefreshingLogs} disabled={isRefreshingLogs}
> >
<RefreshCwIcon <RefreshCwIcon
className={`size-4 transition-transform duration-300 ${isRefreshingLogs ? 'animate-spin' : ''}`} className={`size-4 transition-transform duration-300 ${isRefreshingLogs ? "animate-spin" : ""}`}
/> />
</Button> </Button>
<Button <Button variant="ghost" size="sm" onClick={() => setLogsFullscreenOpen(true)} className="h-8 w-8 p-0">
variant="ghost"
size="sm"
onClick={() => setLogsFullscreenOpen(true)}
className="h-8 w-8 p-0"
>
<MaximizeIcon className="size-4" /> <MaximizeIcon className="size-4" />
</Button> </Button>
</div> </div>
<div ref={logsContainerRef} className={cn("max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm", !logsDisplay && ["animate-pulse", "h-full"])}> <div
ref={logsContainerRef}
className={cn(
"max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm",
!logsDisplay && ["animate-pulse", "h-full"]
)}
>
<div dangerouslySetInnerHTML={{ __html: logsDisplay }} /> <div dangerouslySetInnerHTML={{ __html: logsDisplay }} />
</div> </div>
<div className="flex items-center w-full"> <div className="flex items-center w-full">
@@ -376,15 +389,18 @@ function ContainerSheet({ sheetOpen, setSheetOpen, activeContainer }: { sheetOpe
<MaximizeIcon className="size-4" /> <MaximizeIcon className="size-4" />
</Button> </Button>
</div> </div>
<div className={cn("grow h-[calc(50dvh-4rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm", !infoDisplay && "animate-pulse")}> <div
className={cn(
"grow h-[calc(50dvh-4rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm",
!infoDisplay && "animate-pulse"
)}
>
<div dangerouslySetInnerHTML={{ __html: infoDisplay }} /> <div dangerouslySetInnerHTML={{ __html: infoDisplay }} />
</div> </div>
</div> </div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</> </>
) )
} }
@@ -406,39 +422,51 @@ function ContainersTableHead({ table }: { table: TableType<ContainerRecord> }) {
) )
} }
const ContainerTableRow = memo( const ContainerTableRow = memo(function ContainerTableRow({
function ContainerTableRow({ row,
row, virtualRow,
virtualRow, openSheet,
openSheet, }: {
}: { row: Row<ContainerRecord>
row: Row<ContainerRecord> virtualRow: VirtualItem
virtualRow: VirtualItem openSheet: (container: ContainerRecord) => void
openSheet: (container: ContainerRecord) => void }) {
}) { return (
return ( <TableRow
<TableRow data-state={row.getIsSelected() && "selected"}
data-state={row.getIsSelected() && "selected"} className="cursor-pointer transition-opacity"
className="cursor-pointer transition-opacity" onClick={() => openSheet(row.original)}
onClick={() => openSheet(row.original)} >
> {row.getVisibleCells().map((cell) => (
{row.getVisibleCells().map((cell) => ( <TableCell
<TableCell key={cell.id}
key={cell.id} className="py-0"
className="py-0" style={{
style={{ height: virtualRow.size,
height: virtualRow.size, }}
}} >
> {flexRender(cell.column.columnDef.cell, cell.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell>
</TableCell> ))}
))} </TableRow>
</TableRow> )
) })
}
)
function LogsFullscreenDialog({ open, onOpenChange, logsDisplay, containerName, onRefresh, isRefreshing }: { open: boolean, onOpenChange: (open: boolean) => void, logsDisplay: string, containerName: string, onRefresh: () => void | Promise<void>, isRefreshing: boolean }) { function LogsFullscreenDialog({
open,
onOpenChange,
logsDisplay,
containerName,
onRefresh,
isRefreshing,
}: {
open: boolean
onOpenChange: (open: boolean) => void
logsDisplay: string
containerName: string
onRefresh: () => void | Promise<void>
isRefreshing: boolean
}) {
const outerContainerRef = useRef<HTMLDivElement>(null) const outerContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
@@ -471,16 +499,24 @@ function LogsFullscreenDialog({ open, onOpenChange, logsDisplay, containerName,
title={t`Refresh`} title={t`Refresh`}
aria-label={t`Refresh`} aria-label={t`Refresh`}
> >
<RefreshCwIcon <RefreshCwIcon className={`size-4 transition-transform duration-300 ${isRefreshing ? "animate-spin" : ""}`} />
className={`size-4 transition-transform duration-300 ${isRefreshing ? 'animate-spin' : ''}`}
/>
</button> </button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )
} }
function InfoFullscreenDialog({ open, onOpenChange, infoDisplay, containerName }: { open: boolean, onOpenChange: (open: boolean) => void, infoDisplay: string, containerName: string }) { function InfoFullscreenDialog({
open,
onOpenChange,
infoDisplay,
containerName,
}: {
open: boolean
onOpenChange: (open: boolean) => void
infoDisplay: string
containerName: string
}) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white"> <DialogContent className="w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white">

View File

@@ -699,6 +699,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false) const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}` return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
}} }}
showTotal={true}
/> />
</ChartCard> </ChartCard>
@@ -752,6 +753,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false) const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}` return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
}} }}
showTotal={true}
/> />
</ChartCard> </ChartCard>

View File

@@ -1,8 +1,10 @@
import type { JSX } from "react" import type { JSX } from "react"
import { useLingui } from "@lingui/react/macro"
import * as React from "react" import * as React from "react"
import * as RechartsPrimitive from "recharts" import * as RechartsPrimitive from "recharts"
import { chartTimeData, cn } from "@/lib/utils" import { chartTimeData, cn } from "@/lib/utils"
import type { ChartData } from "@/types" import type { ChartData } from "@/types"
import { Separator } from "./separator"
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const
@@ -100,6 +102,8 @@ const ChartTooltipContent = React.forwardRef<
filter?: string filter?: string
contentFormatter?: (item: any, key: string) => React.ReactNode | string contentFormatter?: (item: any, key: string) => React.ReactNode | string
truncate?: boolean truncate?: boolean
showTotal?: boolean
totalLabel?: React.ReactNode
} }
>( >(
( (
@@ -121,11 +125,16 @@ const ChartTooltipContent = React.forwardRef<
itemSorter, itemSorter,
contentFormatter: content = undefined, contentFormatter: content = undefined,
truncate = false, truncate = false,
showTotal = false,
totalLabel,
}, },
ref ref
) => { ) => {
// const { config } = useChart() // const { config } = useChart()
const config = {} const config = {}
const { t } = useLingui()
const totalLabelNode = totalLabel ?? t`Total`
const totalName = typeof totalLabelNode === "string" ? totalLabelNode : t`Total`
React.useMemo(() => { React.useMemo(() => {
if (filter) { if (filter) {
@@ -141,6 +150,76 @@ const ChartTooltipContent = React.forwardRef<
} }
}, [itemSorter, payload]) }, [itemSorter, payload])
const totalValueDisplay = React.useMemo(() => {
if (!showTotal || !payload?.length) {
return null
}
let totalValue = 0
let hasNumericValue = false
const aggregatedNestedValues: Record<string, number> = {}
for (const item of payload) {
const numericValue = typeof item.value === "number" ? item.value : Number(item.value)
if (Number.isFinite(numericValue)) {
totalValue += numericValue
hasNumericValue = true
}
if (content && item?.payload) {
const payloadKey = `${nameKey || item.name || item.dataKey || "value"}`
const nestedPayload = (item.payload as Record<string, unknown> | undefined)?.[payloadKey]
if (nestedPayload && typeof nestedPayload === "object") {
for (const [nestedKey, nestedValue] of Object.entries(nestedPayload)) {
if (typeof nestedValue === "number" && Number.isFinite(nestedValue)) {
aggregatedNestedValues[nestedKey] = (aggregatedNestedValues[nestedKey] ?? 0) + nestedValue
}
}
}
}
}
if (!hasNumericValue) {
return null
}
const totalKey = "__total__"
const totalItem: any = {
value: totalValue,
name: totalName,
dataKey: totalKey,
color,
}
if (content) {
const basePayload =
payload[0]?.payload && typeof payload[0].payload === "object"
? { ...(payload[0].payload as Record<string, unknown>) }
: {}
totalItem.payload = {
...basePayload,
[totalKey]: aggregatedNestedValues,
}
}
if (typeof formatter === "function") {
return formatter(
totalValue,
totalName,
totalItem,
payload.length,
totalItem.payload ?? payload[0]?.payload
)
}
if (content) {
return content(totalItem, totalKey)
}
return `${totalValue.toLocaleString()}${unit ?? ""}`
}, [color, content, formatter, nameKey, payload, showTotal, totalName, unit])
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null return null
@@ -242,6 +321,15 @@ const ChartTooltipContent = React.forwardRef<
</div> </div>
) )
})} })}
{totalValueDisplay ? (
<>
<Separator className="mt-0.5" />
<div className="flex items-center justify-between gap-2 -mt-0.75 font-medium">
<span className="text-muted-foreground ps-3">{totalLabelNode}</span>
<span>{totalValueDisplay}</span>
</div>
</>
) : null}
</div> </div>
</div> </div>
) )

View File

@@ -1199,6 +1199,11 @@ msgstr "تسمح الرموز المميزة للوكلاء بالاتصال و
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور." msgstr "تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "الإجمالي"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "إجمالي البيانات المستلمة لكل واجهة" msgstr "إجمالي البيانات المستلمة لكل واجهة"

View File

@@ -1199,6 +1199,11 @@ msgstr "Токените позволяват на агентите да се с
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора." msgstr "Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Общо"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Общо получени данни за всеки интерфейс" msgstr "Общо получени данни за всеки интерфейс"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokeny umožňují agentům připojení a registraci. Otisky jsou stabil
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu." msgstr "Tokeny a otisky slouží k ověření připojení WebSocket k uzlu."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Celkem"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Celkový přijatý objem dat pro každé rozhraní" msgstr "Celkový přijatý objem dat pro každé rozhraní"

View File

@@ -1199,6 +1199,11 @@ msgstr "Nøgler tillader agenter at oprette forbindelse og registrere. Fingeraft
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Nøgler og fingeraftryk bruges til at godkende WebSocket-forbindelser til hubben." msgstr "Nøgler og fingeraftryk bruges til at godkende WebSocket-forbindelser til hubben."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Samlet"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Samlet modtaget data for hver interface" msgstr "Samlet modtaget data for hver interface"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fi
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren." msgstr "Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Gesamt"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Empfangene Gesamtdatenmenge je Schnittstelle " msgstr "Empfangene Gesamtdatenmenge je Schnittstelle "

View File

@@ -1194,6 +1194,11 @@ 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." 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." msgstr "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Total data received for each interface" msgstr "Total data received for each interface"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n" "Language: es\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-11-01 17:41\n" "PO-Revision-Date: 2025-11-04 22:13\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -123,7 +123,7 @@ msgstr "Agente"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Alert History" msgid "Alert History"
msgstr "Historial de Alertas" msgstr "Historial de alertas"
#: src/components/alerts/alert-button.tsx #: src/components/alerts/alert-button.tsx
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
@@ -142,7 +142,7 @@ msgstr "Todos los contenedores"
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "All Systems" msgid "All Systems"
msgstr "Todos los Sistemas" msgstr "Todos los sistemas"
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "Are you sure you want to delete {name}?" msgid "Are you sure you want to delete {name}?"
@@ -746,7 +746,7 @@ msgstr "Instrucciones manuales de configuración"
#. Chart select field. Please try to keep this short. #. Chart select field. Please try to keep this short.
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Max 1 min" msgid "Max 1 min"
msgstr "Máx 1 min" msgstr "Máx. 1 min"
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
@@ -756,11 +756,11 @@ msgstr "Memoria"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/lib/alerts.ts #: src/lib/alerts.ts
msgid "Memory Usage" msgid "Memory Usage"
msgstr "Uso de Memoria" msgstr "Uso de memoria"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Memory usage of docker containers" msgid "Memory usage of docker containers"
msgstr "Uso de memoria de los contenedores de Docker" msgstr "Uso de memoria de los contenedores Docker"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
@@ -779,7 +779,7 @@ msgstr "Red"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Network traffic of docker containers" msgid "Network traffic of docker containers"
msgstr "Tráfico de red de los contenedores de Docker" msgstr "Tráfico de red de los contenedores Docker"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
@@ -897,7 +897,7 @@ msgstr "Pausado ({pausedSystemsLength})"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Per-core average utilization" msgid "Per-core average utilization"
msgstr "Utilización promedio por núcleo" msgstr "Uso promedio por núcleo"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "Percentage of time spent in each state" msgid "Percentage of time spent in each state"
@@ -905,36 +905,36 @@ msgstr "Porcentaje de tiempo dedicado a cada estado"
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered." msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
msgstr "Por favor, <0>configure un servidor SMTP</0> para asegurar que las alertas sean entregadas." msgstr "Por favor, <0>configura un servidor SMTP</0> para asegurar que las alertas sean entregadas."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "Please check logs for more details." msgid "Please check logs for more details."
msgstr "Por favor, revise los registros para más detalles." msgstr "Por favor, revisa los registros para más detalles."
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Please check your credentials and try again" msgid "Please check your credentials and try again"
msgstr "Por favor, verifique sus credenciales e intente de nuevo" msgstr "Por favor, verifica tus credenciales e inténtalo de nuevo"
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Please create an admin account" msgid "Please create an admin account"
msgstr "Por favor, cree una cuenta de administrador" msgstr "Por favor, crea una cuenta de administrador"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Please enable pop-ups for this site" msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite las ventanas emergentes para este sitio" msgstr "Por favor, habilita las ventanas emergentes para este sitio"
#: src/lib/api.ts #: src/lib/api.ts
msgid "Please log in again" msgid "Please log in again"
msgstr "Por favor, inicie sesión de nuevo" msgstr "Por favor, inicia sesión de nuevo"
#: src/components/login/auth-form.tsx #: src/components/login/auth-form.tsx
msgid "Please see <0>the documentation</0> for instructions." msgid "Please see <0>the documentation</0> for instructions."
msgstr "Por favor, consulte <0>la documentación</0> para obtener instrucciones." msgstr "Por favor, consulta <0>la documentación</0> para obtener instrucciones."
#: src/components/login/login.tsx #: src/components/login/login.tsx
msgid "Please sign in to your account" msgid "Please sign in to your account"
msgstr "Por favor, inicie sesión en su cuenta" msgstr "Por favor, inicia sesión en tu cuenta"
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Port" msgid "Port"
@@ -952,12 +952,12 @@ msgstr "Utilización precisa en el momento registrado"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Preferred Language" msgid "Preferred Language"
msgstr "Idioma Preferido" msgstr "Idioma preferido"
#. Use 'Key' if your language requires many more characters #. Use 'Key' if your language requires many more characters
#: src/components/add-system.tsx #: src/components/add-system.tsx
msgid "Public Key" msgid "Public Key"
msgstr "Clave Pública" msgstr "Clave pública"
#. Disk read #. Disk read
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -984,7 +984,7 @@ msgstr "Solicitar OTP"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Reset Password" msgid "Reset Password"
msgstr "Restablecer Contraseña" msgstr "Restablecer contraseña"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -1014,7 +1014,7 @@ msgstr "Autoprueba S.M.A.R.T."
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
msgid "Save address using enter key or comma. Leave blank to disable email notifications." msgid "Save address using enter key or comma. Leave blank to disable email notifications."
msgstr "Guarde la dirección usando la tecla enter o coma. Deje en blanco para desactivar las notificaciones por correo." msgstr "Guarda la dirección usando la tecla enter o coma. Deja en blanco para desactivar las notificaciones por correo."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
#: src/components/routes/settings/notifications.tsx #: src/components/routes/settings/notifications.tsx
@@ -1035,7 +1035,7 @@ msgstr "Buscar sistemas o configuraciones..."
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "See <0>notification settings</0> to configure how you receive alerts." msgid "See <0>notification settings</0> to configure how you receive alerts."
msgstr "Consulte <0>configuración de notificaciones</0> para configurar cómo recibe alertas." msgstr "Consulta <0>configuración de notificaciones</0> para configurar cómo recibe alertas."
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Sent" msgid "Sent"
@@ -1090,7 +1090,7 @@ msgstr "Espacio de swap utilizado por el sistema"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
msgid "Swap Usage" msgid "Swap Usage"
msgstr "Uso de Swap" msgstr "Uso de swap"
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
#: src/components/containers-table/containers-table-columns.tsx #: src/components/containers-table/containers-table-columns.tsx
@@ -1110,7 +1110,7 @@ msgstr "Sistemas"
#: src/components/routes/settings/config-yaml.tsx #: src/components/routes/settings/config-yaml.tsx
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory." msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
msgstr "Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dentro de su directorio de datos." msgstr "Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dentro de tu directorio de datos."
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Table" msgid "Table"
@@ -1145,7 +1145,7 @@ msgstr "Notificación de prueba enviada"
#: src/components/login/forgot-pass-form.tsx #: src/components/login/forgot-pass-form.tsx
msgid "Then log into the backend and reset your user account password in the users table." msgid "Then log into the backend and reset your user account password in the users table."
msgstr "Luego inicie sesión en el backend y restablezca la contraseña de su cuenta de usuario en la tabla de usuarios." msgstr "Luego inicia sesión en el backend y restablece la contraseña de tu cuenta de usuario en la tabla de usuarios."
#: src/components/systems-table/systems-table-columns.tsx #: src/components/systems-table/systems-table-columns.tsx
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database." msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
@@ -1189,7 +1189,7 @@ msgstr "Token"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens & Fingerprints" msgid "Tokens & Fingerprints"
msgstr "Tokens y Huellas Digitales" msgstr "Tokens y huellas digitales"
#: src/components/routes/settings/tokens-fingerprints.tsx #: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection." msgid "Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection."
@@ -1199,6 +1199,11 @@ 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." 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." msgstr "Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Datos totales recibidos por cada interfaz" msgstr "Datos totales recibidos por cada interfaz"
@@ -1321,7 +1326,7 @@ msgstr "Ver más"
#: src/components/routes/settings/alerts-history-data-table.tsx #: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts." msgid "View your 200 most recent alerts."
msgstr "Ver sus 200 alertas más recientes." msgstr "Ver tus 200 alertas más recientes."
#: src/components/systems-table/systems-table.tsx #: src/components/systems-table/systems-table.tsx
msgid "Visible Fields" msgid "Visible Fields"
@@ -1333,7 +1338,7 @@ msgstr "Esperando suficientes registros para mostrar"
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Want to help improve our translations? Check <0>Crowdin</0> for details." msgid "Want to help improve our translations? Check <0>Crowdin</0> for details."
msgstr "¿Quieres ayudarnos a mejorar nuestras traducciones? Consulta <0>Crowdin</0> para más detalles." msgstr "¿Quieres ayudar a mejorar nuestras traducciones? Consulta <0>Crowdin</0> para más detalles."
#: src/components/routes/settings/general.tsx #: src/components/routes/settings/general.tsx
msgid "Warning (%)" msgid "Warning (%)"
@@ -1373,4 +1378,4 @@ msgstr "Configuración YAML"
#: src/components/routes/settings/layout.tsx #: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated." msgid "Your user settings have been updated."
msgstr "Su configuración de usuario ha sido actualizada." msgstr "Tu configuración de usuario ha sido actualizada."

View File

@@ -1199,6 +1199,11 @@ msgstr "توکن‌ها به عامل‌ها اجازه اتصال و ثبت‌
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "توکن‌ها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده می‌شوند." msgstr "توکن‌ها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده می‌شوند."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "کل"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "داده‌های کل دریافت شده برای هر رابط" msgstr "داده‌های کل دریافت شده برای هر رابط"

View File

@@ -1199,6 +1199,11 @@ 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." 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." msgstr "Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Données totales reçues pour chaque interface" msgstr "Données totales reçues pour chaque interface"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokens מאפשרים לסוכנים להתחבר ולהירשם. טבי
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens וטביעות אצבע משמשים לאימות חיבורי WebSocket ל-hub." msgstr "Tokens וטביעות אצבע משמשים לאימות חיבורי WebSocket ל-hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "כולל"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "סך נתונים שהתקבלו עבור כל ממשק" msgstr "סך נתונים שהתקבלו עבור כל ממשק"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokeni dopuštaju agentima prijavu i registraciju. Otisci su stabilni id
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeni se uz otiske koriste za autentifikaciju WebSocket veza prema središnjoj kontroli." msgstr "Tokeni se uz otiske koriste za autentifikaciju WebSocket veza prema središnjoj kontroli."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Ukupno"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Ukupni podaci primljeni za svako sučelje" msgstr "Ukupni podaci primljeni za svako sučelje"

View File

@@ -1199,6 +1199,11 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "" msgstr ""
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Összesen"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Összes fogadott adat minden interfészenként" msgstr "Összes fogadott adat minden interfészenként"

View File

@@ -1199,6 +1199,11 @@ 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." 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." msgstr "I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Totale"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Dati totali ricevuti per ogni interfaccia" msgstr "Dati totali ricevuti per ogni interfaccia"

View File

@@ -1199,6 +1199,11 @@ msgstr "トークンはエージェントの接続と登録を可能にします
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。" msgstr "トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "総数"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "各インターフェースの総受信データ量" msgstr "各インターフェースの総受信データ量"

View File

@@ -1199,6 +1199,11 @@ msgstr "토큰은 에이전트가 연결하고 등록할 수 있도록 합니다
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는 데 사용됩니다." msgstr "토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는 데 사용됩니다."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "총"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "각 인터페이스별 총합 다운로드 데이터량" msgstr "각 인터페이스별 총합 다운로드 데이터량"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokens staan agenten toe om verbinding te maken met en te registreren. V
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub." msgstr "Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Totaal"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Totaal ontvangen gegevens per interface" msgstr "Totaal ontvangen gegevens per interface"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokens lar agenter koble til og registrere seg selv. Fingeravtrykk er st
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens og fingeravtrykk blir brukt for å autentisere WebSocket-tilkoblinger til huben." msgstr "Tokens og fingeravtrykk blir brukt for å autentisere WebSocket-tilkoblinger til huben."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Totalt mottatt data for hvert grensesnitt" msgstr "Totalt mottatt data for hvert grensesnitt"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokeny umożliwiają agentom łączenie się i rejestrację. Odciski pal
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokeny i odciski palców (fingerprinty) służą do uwierzytelniania połączeń WebSocket z hubem." msgstr "Tokeny i odciski palców (fingerprinty) służą do uwierzytelniania połączeń WebSocket z hubem."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Łącznie"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Całkowita ilość danych odebranych dla każdego interfejsu" msgstr "Całkowita ilość danych odebranych dla każdego interfejsu"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n" "Language: pt\n"
"Project-Id-Version: beszel\n" "Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-10-30 21:52\n" "PO-Revision-Date: 2025-11-04 22:13\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Portuguese\n" "Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -134,7 +134,7 @@ msgstr "Alertas"
#: src/components/containers-table/containers-table.tsx #: src/components/containers-table/containers-table.tsx
#: src/components/routes/containers.tsx #: src/components/routes/containers.tsx
msgid "All Containers" msgid "All Containers"
msgstr "Todos os contentores" msgstr "Todos os Contêineres"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
#: src/components/command-palette.tsx #: src/components/command-palette.tsx
@@ -372,11 +372,11 @@ msgstr "CPU"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Cores" msgid "CPU Cores"
msgstr "Núcleos da CPU" msgstr "Núcleos de CPU"
#: src/components/routes/system/cpu-sheet.tsx #: src/components/routes/system/cpu-sheet.tsx
msgid "CPU Time Breakdown" msgid "CPU Time Breakdown"
msgstr "Detalhamento do tempo da CPU" msgstr "Distribuição do Tempo de CPU"
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
#: src/components/routes/system.tsx #: src/components/routes/system.tsx
@@ -606,7 +606,7 @@ msgstr "Impressão digital"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Firmware" msgid "Firmware"
msgstr "" msgstr "Firmware"
#: src/components/alerts/alerts-sheet.tsx #: src/components/alerts/alerts-sheet.tsx
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}" msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
@@ -764,7 +764,7 @@ msgstr "Uso de memória dos contêineres Docker"
#: src/components/routes/system/smart-table.tsx #: src/components/routes/system/smart-table.tsx
msgid "Model" msgid "Model"
msgstr "" msgstr "Modelo"
#: src/components/add-system.tsx #: src/components/add-system.tsx
#: src/components/alerts-history-columns.tsx #: src/components/alerts-history-columns.tsx
@@ -1199,6 +1199,11 @@ 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." 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." msgstr "Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Dados totais recebidos para cada interface" msgstr "Dados totais recebidos para cada interface"

View File

@@ -1199,6 +1199,11 @@ msgstr "Токены позволяют агентам подключаться
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом." msgstr "Токены и отпечатки используются для аутентификации соединений WebSocket с хабом."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Итого"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Общий объем полученных данных для каждого интерфейса" msgstr "Общий объем полученных данных для каждого интерфейса"

View File

@@ -1199,6 +1199,11 @@ msgstr "Žetoni omogočajo agentom povezavo in registracijo. Prstni odtisi so st
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Žetoni in prstni odtisi se uporabljajo za preverjanje pristnosti WebSocket povezav do vozlišča." msgstr "Žetoni in prstni odtisi se uporabljajo za preverjanje pristnosti WebSocket povezav do vozlišča."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Skupaj"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Skupni prejeti podatki za vsak vmesnik" msgstr "Skupni prejeti podatki za vsak vmesnik"

View File

@@ -1199,6 +1199,11 @@ msgstr "Tokens tillåter agenter att ansluta och registrera. Fingeravtryck är s
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Tokens och fingeravtryck används för att autentisera WebSocket-anslutningar till hubben." msgstr "Tokens och fingeravtryck används för att autentisera WebSocket-anslutningar till hubben."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Total"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Totalt mottagen data för varje gränssnitt" msgstr "Totalt mottagen data för varje gränssnitt"

View File

@@ -1199,6 +1199,11 @@ 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." 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." msgstr "Token'lar ve parmak izleri hub'a WebSocket bağlantılarını doğrulamak için kullanılır."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Toplam"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Her arayüz için alınan toplam veri" msgstr "Her arayüz için alınan toplam veri"

View File

@@ -1199,6 +1199,11 @@ msgstr "Токени дозволяють агентам підключатис
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Токени та відбитки використовуються для автентифікації WebSocket з'єднань до хабу." msgstr "Токени та відбитки використовуються для автентифікації WebSocket з'єднань до хабу."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Разом"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Загальний обсяг отриманих даних для кожного інтерфейсу" msgstr "Загальний обсяг отриманих даних для кожного інтерфейсу"

View File

@@ -1199,6 +1199,11 @@ msgstr "Token cho phép các tác nhân kết nối và đăng ký. Vân tay là
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "Token và vân tay được sử dụng để xác thực các kết nối WebSocket đến trung tâm." msgstr "Token và vân tay được sử dụng để xác thực các kết nối WebSocket đến trung tâm."
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "Tổng"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "Tổng dữ liệu nhận được cho mỗi giao diện" msgstr "Tổng dữ liệu nhận được cho mỗi giao diện"

View File

@@ -1199,6 +1199,11 @@ msgstr "令牌允许客户端连接和注册。指纹是每个系统唯一的稳
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌与指纹用于验证到中心的 WebSocket 连接。" msgstr "令牌与指纹用于验证到中心的 WebSocket 连接。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "总计"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "每个接口的总接收数据量" msgstr "每个接口的总接收数据量"

View File

@@ -1199,6 +1199,11 @@ msgstr "令牌允許代理程式連接和註冊。指紋是每個系統唯一的
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌和指紋用於驗證到中心的WebSocket連接。" msgstr "令牌和指紋用於驗證到中心的WebSocket連接。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "總計"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "每個介面的總接收資料量" msgstr "每個介面的總接收資料量"

View File

@@ -1199,6 +1199,11 @@ msgstr "令牌允許代理程式連線和註冊。指紋是每個系統的唯一
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub." msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr "令牌和指紋被用於驗證到 Hub 的 WebSocket 連線。" msgstr "令牌和指紋被用於驗證到 Hub 的 WebSocket 連線。"
#: src/components/ui/chart.tsx
#: src/components/ui/chart.tsx
msgid "Total"
msgstr "總計"
#: src/components/routes/system/network-sheet.tsx #: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface" msgid "Total data received for each interface"
msgstr "每個介面的總接收資料量" msgstr "每個介面的總接收資料量"

View File

@@ -1,3 +1,17 @@
## 0.15.4
- Refactor containers table to fix clock issue causing no results. (#1337)
- Fix Windows extra disk detection. (#1361)
- Add total line to the tooltip of charts with multiple values. (#1280)
- Add fallback paths for `smartctl` lookup. (#1362, #1363)
- Fix `intel_gpu_top` parsing when engine instance id is in column. (#1230)
- Update `henrygd/beszel-agent-nvidia` Dockerfile to build latest smartmontools. (#1335)
## 0.15.3 ## 0.15.3
- Add CPU state details and per-core usage. (#1356) - Add CPU state details and per-core usage. (#1356)