Compare commits

...

10 Commits

Author SHA1 Message Date
henrygd
240e75f025 add sorted style to home table header buttons 2025-09-21 19:23:34 -04:00
henrygd
ea984844ff update changelog 2025-09-21 17:56:35 -04:00
henrygd
0d157b5857 display agent connection type in hub (ssh, websocket) 2025-09-21 17:49:22 -04:00
henrygd
d0b6e725c8 fix positioning of bandwidth chart button 2025-09-19 12:08:41 -04:00
henrygd
ffe7f8547a fix: update temperature and byte formatting functions to use loose equality checks (#1180) 2025-09-19 11:51:27 -04:00
henrygd
37817b0f15 add --auto-update flag to hub install script 2025-09-18 17:51:22 -04:00
henrygd
a66ac418ae install: remove additional service restart for openwrt 2025-09-18 14:05:19 -04:00
henrygd
2ee2f53267 fix: resolve mipsle architecture detection for install script (#1176)
- Add proper endianness detection using ELF header inspection
- Prevent mipsle devices from downloading incorrect mips binaries
- Maintain backward compatibility for all other architectures
2025-09-18 13:48:24 -04:00
henrygd
e5c766c00b refactoring
- network interface delta
- string concatenation
2025-09-17 21:36:05 -04:00
henrygd
da43ba10e1 add aria-label to button in NetworkSheet for improved accessibility 2025-09-17 16:23:02 -04:00
15 changed files with 275 additions and 104 deletions

View File

@@ -9,19 +9,21 @@ import (
"time" "time"
"github.com/henrygd/beszel/agent/health" "github.com/henrygd/beszel/agent/health"
"github.com/henrygd/beszel/internal/entities/system"
) )
// ConnectionManager manages the connection state and events for the agent. // ConnectionManager manages the connection state and events for the agent.
// It handles both WebSocket and SSH connections, automatically switching between // It handles both WebSocket and SSH connections, automatically switching between
// them based on availability and managing reconnection attempts. // them based on availability and managing reconnection attempts.
type ConnectionManager struct { type ConnectionManager struct {
agent *Agent // Reference to the parent agent agent *Agent // Reference to the parent agent
State ConnectionState // Current connection state State ConnectionState // Current connection state
eventChan chan ConnectionEvent // Channel for connection events eventChan chan ConnectionEvent // Channel for connection events
wsClient *WebSocketClient // WebSocket client for hub communication wsClient *WebSocketClient // WebSocket client for hub communication
serverOptions ServerOptions // Configuration for SSH server serverOptions ServerOptions // Configuration for SSH server
wsTicker *time.Ticker // Ticker for WebSocket connection attempts wsTicker *time.Ticker // Ticker for WebSocket connection attempts
isConnecting bool // Prevents multiple simultaneous reconnection attempts isConnecting bool // Prevents multiple simultaneous reconnection attempts
ConnectionType system.ConnectionType
} }
// ConnectionState represents the current connection state of the agent. // ConnectionState represents the current connection state of the agent.
@@ -144,15 +146,18 @@ func (c *ConnectionManager) handleStateChange(newState ConnectionState) {
switch newState { switch newState {
case WebSocketConnected: case WebSocketConnected:
slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host) slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host)
c.ConnectionType = system.ConnectionTypeWebSocket
c.stopWsTicker() c.stopWsTicker()
_ = c.agent.StopServer() _ = c.agent.StopServer()
c.isConnecting = false c.isConnecting = false
case SSHConnected: case SSHConnected:
// stop new ws connection attempts // stop new ws connection attempts
slog.Info("SSH connection established") slog.Info("SSH connection established")
c.ConnectionType = system.ConnectionTypeSSH
c.stopWsTicker() c.stopWsTicker()
c.isConnecting = false c.isConnecting = false
case Disconnected: case Disconnected:
c.ConnectionType = system.ConnectionTypeNone
if c.isConnecting { if c.isConnecting {
// Already handling reconnection, avoid duplicate attempts // Already handling reconnection, avoid duplicate attempts
return return

View File

@@ -6,10 +6,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
psutilNet "github.com/shirou/gopsutil/v4/net" psutilNet "github.com/shirou/gopsutil/v4/net"
) )
var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]()
func (a *Agent) updateNetworkStats(systemStats *system.Stats) { func (a *Agent) updateNetworkStats(systemStats *system.Stats) {
// network stats // network stats
if len(a.netInterfaces) == 0 { if len(a.netInterfaces) == 0 {
@@ -40,12 +43,13 @@ func (a *Agent) updateNetworkStats(systemStats *system.Stats) {
totalBytesRecv += v.BytesRecv totalBytesRecv += v.BytesRecv
// track deltas for each network interface // track deltas for each network interface
netInterfaceDeltaTracker.Set(fmt.Sprintf("%sdown", v.Name), v.BytesRecv)
netInterfaceDeltaTracker.Set(fmt.Sprintf("%sup", v.Name), v.BytesSent)
var upDelta, downDelta uint64 var upDelta, downDelta uint64
upKey, downKey := fmt.Sprintf("%sup", v.Name), fmt.Sprintf("%sdown", v.Name)
netInterfaceDeltaTracker.Set(upKey, v.BytesSent)
netInterfaceDeltaTracker.Set(downKey, v.BytesRecv)
if msElapsed > 0 { if msElapsed > 0 {
upDelta = netInterfaceDeltaTracker.Delta(fmt.Sprintf("%sup", v.Name)) * 1000 / msElapsed upDelta = netInterfaceDeltaTracker.Delta(upKey) * 1000 / msElapsed
downDelta = netInterfaceDeltaTracker.Delta(fmt.Sprintf("%sdown", v.Name)) * 1000 / msElapsed downDelta = netInterfaceDeltaTracker.Delta(downKey) * 1000 / msElapsed
} }
// add interface to systemStats // add interface to systemStats
systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv} systemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}

View File

@@ -11,7 +11,6 @@ import (
"github.com/henrygd/beszel" "github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery" "github.com/henrygd/beszel/agent/battery"
"github.com/henrygd/beszel/agent/deltatracker"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
@@ -21,8 +20,6 @@ import (
"github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/mem"
) )
var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]()
// Sets initial / non-changing values about the host system // Sets initial / non-changing values about the host system
func (a *Agent) initializeSystemInfo() { func (a *Agent) initializeSystemInfo() {
a.systemInfo.AgentVersion = beszel.Version a.systemInfo.AgentVersion = beszel.Version
@@ -34,7 +31,7 @@ func (a *Agent) initializeSystemInfo() {
a.systemInfo.KernelVersion = version a.systemInfo.KernelVersion = version
a.systemInfo.Os = system.Darwin a.systemInfo.Os = system.Darwin
} else if strings.Contains(platform, "indows") { } else if strings.Contains(platform, "indows") {
a.systemInfo.KernelVersion = strings.Replace(platform, "Microsoft ", "", 1) + " " + version a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version)
a.systemInfo.Os = system.Windows a.systemInfo.Os = system.Windows
} else if platform == "freebsd" { } else if platform == "freebsd" {
a.systemInfo.Os = system.Freebsd a.systemInfo.Os = system.Freebsd
@@ -215,6 +212,7 @@ func (a *Agent) getSystemStats() system.Stats {
} }
// update base system info // update base system info
a.systemInfo.ConnectionType = a.connectionManager.ConnectionType
a.systemInfo.Cpu = systemStats.Cpu a.systemInfo.Cpu = systemStats.Cpu
a.systemInfo.LoadAvg = systemStats.LoadAvg a.systemInfo.LoadAvg = systemStats.LoadAvg
// TODO: remove these in future release in favor of load avg array // TODO: remove these in future release in favor of load avg array

View File

@@ -84,6 +84,14 @@ const (
Freebsd Freebsd
) )
type ConnectionType = uint8
const (
ConnectionTypeNone ConnectionType = iota
ConnectionTypeSSH
ConnectionTypeWebSocket
)
type Info struct { type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"` Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
@@ -105,7 +113,8 @@ type Info struct {
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
// TODO: remove load fields in future release in favor of load avg array // TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
} }
// Final data structure to return to the hub // Final data structure to return to the hub

View File

@@ -3,7 +3,15 @@ import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { getPagePath } from "@nanostores/router" import { getPagePath } from "@nanostores/router"
import { timeTicks } from "d3-time" import { timeTicks } from "d3-time"
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react" import {
ChevronRightSquareIcon,
ClockArrowUp,
CpuIcon,
GlobeIcon,
LayoutGridIcon,
MonitorIcon,
XIcon,
} from "lucide-react"
import { subscribeKeys } from "nanostores" import { subscribeKeys } from "nanostores"
import React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import AreaChartDefault from "@/components/charts/area-chart" import AreaChartDefault from "@/components/charts/area-chart"
@@ -16,7 +24,7 @@ import MemChart from "@/components/charts/mem-chart"
import SwapChart from "@/components/charts/swap-chart" import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart" import TemperatureChart from "@/components/charts/temperature-chart"
import { getPbTimestamp, pb } from "@/lib/api" import { getPbTimestamp, pb } from "@/lib/api"
import { ChartType, Os, SystemStatus, Unit } from "@/lib/enums" import { ChartType, ConnectionType, Os, SystemStatus, Unit } from "@/lib/enums"
import { batteryStateTranslations } from "@/lib/i18n" import { batteryStateTranslations } from "@/lib/i18n"
import { import {
$allSystemsByName, $allSystemsByName,
@@ -47,7 +55,7 @@ import { $router, navigate } from "../router"
import Spinner from "../spinner" import Spinner from "../spinner"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WindowsIcon } from "../ui/icons" import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
@@ -130,7 +138,7 @@ async function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(
function dockerOrPodman(str: string, system: SystemRecord) { function dockerOrPodman(str: string, system: SystemRecord) {
if (system.info.p) { if (system.info.p) {
str = str.replace("docker", "podman").replace("Docker", "Podman") return str.replace("docker", "podman").replace("Docker", "Podman")
} }
return str return str
} }
@@ -407,25 +415,45 @@ export default memo(function SystemDetail({ name }: { name: string }) {
<div> <div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1> <h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90"> <div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<div className="capitalize flex gap-2 items-center"> <TooltipProvider>
<span className={cn("relative flex h-3 w-3")}> <Tooltip>
{system.status === SystemStatus.Up && ( <TooltipTrigger asChild>
<span <div className="capitalize flex gap-2 items-center">
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" <span className={cn("relative flex h-3 w-3")}>
style={{ animationDuration: "1.5s" }} {system.status === SystemStatus.Up && (
></span> <span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: "1.5s" }}
></span>
)}
<span
className={cn("relative inline-flex rounded-full h-3 w-3", {
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
</TooltipTrigger>
{system.info.ct && (
<TooltipContent>
{system.info.ct === ConnectionType.WebSocket ? (
<div className="flex gap-1 items-center">
<WebSocketIcon className="size-4" /> WebSocket
</div>
) : (
<div className="flex gap-1 items-center">
<ChevronRightSquareIcon className="size-4" strokeWidth={2} /> SSH
</div>
)}
</TooltipContent>
)} )}
<span </Tooltip>
className={cn("relative inline-flex rounded-full h-3 w-3", { </TooltipProvider>
"bg-green-500": system.status === SystemStatus.Up,
"bg-red-500": system.status === SystemStatus.Down,
"bg-primary/40": system.status === SystemStatus.Paused,
"bg-yellow-500": system.status === SystemStatus.Pending,
})}
></span>
</span>
{translatedStatus}
</div>
{systemInfo.map(({ value, label, Icon, hide }) => { {systemInfo.map(({ value, label, Icon, hide }) => {
if (hide || !value) { if (hide || !value) {
return null return null

View File

@@ -41,9 +41,10 @@ export default memo(function NetworkSheet({
<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}> <Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
aria-label={t`View more`}
variant="outline" variant="outline"
size="icon" size="icon"
className="shrink-0 absolute top-3 end-3 sm:inline-flex sm:top-0 sm:end-0" className="shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3"
> >
<MoreHorizontalIcon /> <MoreHorizontalIcon />
</Button> </Button>

View File

@@ -6,6 +6,7 @@ import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-tabl
import type { ClassValue } from "clsx" import type { ClassValue } from "clsx"
import { import {
ArrowUpDownIcon, ArrowUpDownIcon,
ChevronRightSquareIcon,
CopyIcon, CopyIcon,
CpuIcon, CpuIcon,
HardDriveIcon, HardDriveIcon,
@@ -20,7 +21,7 @@ import {
} from "lucide-react" } from "lucide-react"
import { memo, useMemo, useRef, useState } from "react" import { memo, useMemo, useRef, useState } from "react"
import { isReadOnlyUser, pb } from "@/lib/api" import { isReadOnlyUser, pb } from "@/lib/api"
import { MeterState, SystemStatus } from "@/lib/enums" import { ConnectionType, MeterState, SystemStatus } from "@/lib/enums"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores" import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import { import {
cn, cn,
@@ -54,7 +55,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu" } from "../ui/dropdown-menu"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons" import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon, WebSocketIcon } from "../ui/icons"
const STATUS_COLORS = { const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500", [SystemStatus.Up]: "bg-green-500",
@@ -271,18 +272,18 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
return null return null
} }
const system = info.row.original const system = info.row.original
const color = {
"text-green-500": version === globalThis.BESZEL.HUB_VERSION,
"text-yellow-500": version !== globalThis.BESZEL.HUB_VERSION,
"text-red-500": system.status !== SystemStatus.Up,
}
return ( return (
<span className={cn("flex gap-1.5 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}> <div className={cn("flex gap-1.5 items-center md:pe-5 tabular-nums", viewMode === "table" && "ps-0.5")}>
<IndicatorDot {system.info.ct === ConnectionType.WebSocket && <WebSocketIcon className={cn("size-3", color)} />}
system={system} {system.info.ct === ConnectionType.SSH && <ChevronRightSquareIcon className={cn("size-3", color)} />}
className={ {!system.info.ct && <IndicatorDot system={system} className={cn(color, "bg-current mx-0.5")} />}
(system.status !== SystemStatus.Up && STATUS_COLORS[SystemStatus.Paused]) ||
(version === globalThis.BESZEL.HUB_VERSION && STATUS_COLORS[SystemStatus.Up]) ||
STATUS_COLORS[SystemStatus.Pending]
}
/>
<span className="truncate max-w-14">{info.getValue() as string}</span> <span className="truncate max-w-14">{info.getValue() as string}</span>
</span> </div>
) )
}, },
}, },
@@ -305,10 +306,11 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
const { column } = context const { column } = context
// @ts-expect-error // @ts-expect-error
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
const isSorted = column.getIsSorted()
return ( return (
<Button <Button
variant="ghost" variant="ghost"
className="h-9 px-3 flex" className={cn("h-9 px-3 flex duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
> >
{Icon && <Icon className="me-2 size-4" />} {Icon && <Icon className="me-2 size-4" />}

View File

@@ -337,7 +337,7 @@ const AllSystemsTable = memo(
{/* add header height to table size */} {/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}> <div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full"> <table className="text-sm w-full h-full">
<SystemsTableHead table={table} colLength={colLength} /> <SystemsTableHead table={table} />
<TableBody onMouseEnter={preloadSystemDetail}> <TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? ( {rows.length ? (
virtualRows.map((virtualRow) => { virtualRows.map((virtualRow) => {
@@ -367,26 +367,23 @@ const AllSystemsTable = memo(
} }
) )
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) { function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
const { i18n } = useLingui() const { t } = useLingui()
return (
return useMemo(() => { <TableHeader className="sticky top-0 z-20 w-full border-b-2">
return ( {table.getHeaderGroups().map((headerGroup) => (
<TableHeader className="sticky top-0 z-20 w-full border-b-2"> <tr key={headerGroup.id}>
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => {
<tr key={headerGroup.id}> return (
{headerGroup.headers.map((header) => { <TableHead className="px-1.5" key={header.id}>
return ( {flexRender(header.column.columnDef.header, header.getContext())}
<TableHead className="px-1.5" key={header.id}> </TableHead>
{flexRender(header.column.columnDef.header, header.getContext())} )
</TableHead> })}
) </tr>
})} ))}
</tr> </TableHeader>
))} )
</TableHeader>
)
}, [i18n.locale, colLength])
} }
const SystemTableRow = memo( const SystemTableRow = memo(

View File

@@ -130,3 +130,12 @@ export function HourglassIcon(props: SVGProps<SVGSVGElement>) {
</svg> </svg>
) )
} }
export function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 256 193" {...props} fill="currentColor">
<title>WebSocket</title>
<path d="M192 145h32V68l-36-35-22 22 26 27zm32 16H113l-26-27 11-11 22 22h45l-44-45 11-11 44 44V88l-21-22 11-11-55-55H0l32 32h65l24 23-34 34-24-23V48H32v31l55 55-23 22 36 36h156z" />
</svg>
)
}

View File

@@ -53,3 +53,9 @@ export enum HourFormat {
"12h" = "12h", "12h" = "12h",
"24h" = "24h", "24h" = "24h",
} }
/** Connection type */
export enum ConnectionType {
SSH = 1,
WebSocket,
}

View File

@@ -179,8 +179,8 @@ export function formatTemperature(celsius: number, unit?: Unit): { value: number
if (!unit) { if (!unit) {
unit = $userSettings.get().unitTemp || Unit.Celsius unit = $userSettings.get().unitTemp || Unit.Celsius
} }
// need loose equality check due to form data being strings // biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings
if (unit === Unit.Fahrenheit) { if (unit == Unit.Fahrenheit) {
return { return {
value: celsius * 1.8 + 32, value: celsius * 1.8 + 32,
unit: "°F", unit: "°F",
@@ -202,8 +202,8 @@ export function formatBytes(
// Convert MB to bytes if isMegabytes is true // Convert MB to bytes if isMegabytes is true
if (isMegabytes) size *= 1024 * 1024 if (isMegabytes) size *= 1024 * 1024
// need loose equality check due to form data being strings // biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings
if (unit === Unit.Bits) { if (unit == Unit.Bits) {
const bits = size * 8 const bits = size * 8
const suffix = perSecond ? "ps" : "" const suffix = perSecond ? "ps" : ""
if (bits < 1000) return { value: bits, unit: `b${suffix}` } if (bits < 1000) return { value: bits, unit: `b${suffix}` }

View File

@@ -1,5 +1,5 @@
import type { RecordModel } from "pocketbase" import type { RecordModel } from "pocketbase"
import type { Unit, Os, BatteryState, HourFormat } from "./lib/enums" import type { Unit, Os, BatteryState, HourFormat, ConnectionType } from "@/lib/enums"
// global window properties // global window properties
declare global { declare global {
@@ -75,6 +75,8 @@ export interface SystemInfo {
dt?: number dt?: number
/** operating system */ /** operating system */
os?: Os os?: Os
/** connection type */
ct?: ConnectionType
} }
export interface SystemStats { export interface SystemStats {

View File

@@ -1,3 +1,9 @@
## 0.12.10
- Show connection type (WebSocket / SSH) in hub UI.
- Fix temperature unit and bytes / bits settings. (#1180)
## 0.12.9 ## 0.12.9
- Fix divide by zero error introduced in 0.12.8 :) (#1175) - Fix divide by zero error introduced in 0.12.8 :) (#1175)

View File

@@ -161,6 +161,53 @@ run_rc_command "$1"
EOF EOF
} }
# Detect system architecture
detect_architecture() {
local arch=$(uname -m)
if [ "$arch" = "mips" ]; then
detect_mips_endianness
return $?
fi
case "$arch" in
x86_64)
arch="amd64"
;;
armv6l|armv7l)
arch="arm"
;;
aarch64)
arch="arm64"
;;
esac
echo "$arch"
}
# Detect MIPS endianness using ELF header
detect_mips_endianness() {
local bins="/bin/sh /bin/ls /usr/bin/env"
local bin_to_check endian
for bin_to_check in $bins; do
if [ -f "$bin_to_check" ]; then
# The 6th byte in ELF header: 01 = little, 02 = big
endian=$(hexdump -n 1 -s 5 -e '1/1 "%02x"' "$bin_to_check" 2>/dev/null)
if [ "$endian" = "01" ]; then
echo "mipsle"
return
elif [ "$endian" = "02" ]; then
echo "mips"
return
fi
fi
done
# Final fallback
echo "mips"
}
# Default values # Default values
PORT=45876 PORT=45876
UNINSTALL=false UNINSTALL=false
@@ -556,7 +603,7 @@ fi
echo "Downloading and installing the agent..." echo "Downloading and installing the agent..."
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/') OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/armv6l/arm/' -e 's/armv7l/arm/' -e 's/aarch64/arm64/') ARCH=$(detect_architecture)
FILE_NAME="beszel-agent_${OS}_${ARCH}.tar.gz" FILE_NAME="beszel-agent_${OS}_${ARCH}.tar.gz"
# Determine version to install # Determine version to install
@@ -738,9 +785,7 @@ EXTRA_HELP=" update Update the Beszel agent
restart Restart the Beszel agent" restart Restart the Beszel agent"
update() { update() {
if $BIN_PATH update | grep -q "Update completed successfully"; then $BIN_PATH update
/etc/init.d/beszel-agent restart
fi
} }
EOF EOF

View File

@@ -16,6 +16,7 @@ fi
version=0.0.1 version=0.0.1
PORT=8090 # Default port PORT=8090 # Default port
GITHUB_PROXY_URL="https://ghfast.top/" # Default proxy URL GITHUB_PROXY_URL="https://ghfast.top/" # Default proxy URL
AUTO_UPDATE_FLAG="false" # default to no auto-updates, "true" means enable
# Function to ensure the proxy URL ends with a / # Function to ensure the proxy URL ends with a /
ensure_trailing_slash() { ensure_trailing_slash() {
@@ -32,26 +33,42 @@ ensure_trailing_slash() {
# Ensure the proxy URL ends with a / # Ensure the proxy URL ends with a /
GITHUB_PROXY_URL=$(ensure_trailing_slash "$GITHUB_PROXY_URL") GITHUB_PROXY_URL=$(ensure_trailing_slash "$GITHUB_PROXY_URL")
# Read command line options # Parse command line arguments
while getopts ":uhp:c:" opt; do while [ $# -gt 0 ]; do
case $opt in case "$1" in
u) UNINSTALL="true" ;; -u)
h) UNINSTALL="true"
printf "Beszel Hub installation script\n\n" shift
printf "Usage: ./install-hub.sh [options]\n\n" ;;
printf "Options: \n" -h|--help)
printf " -u : Uninstall the Beszel Hub\n" printf "Beszel Hub installation script\n\n"
printf " -p <port> : Specify a port number (default: 8090)\n" printf "Usage: ./install-hub.sh [options]\n\n"
printf " -c <url> : Use a custom GitHub mirror URL (e.g., https://ghfast.top/)\n" printf "Options: \n"
echo " -h : Display this help message" printf " -u : Uninstall the Beszel Hub\n"
exit 0 printf " -p <port> : Specify a port number (default: 8090)\n"
;; printf " -c <url> : Use a custom GitHub mirror URL (e.g., https://ghfast.top/)\n"
p) PORT=$OPTARG ;; printf " --auto-update : Enable automatic daily updates (disabled by default)\n"
c) GITHUB_PROXY_URL=$(ensure_trailing_slash "$OPTARG") ;; printf " -h, --help : Display this help message\n"
\?) exit 0
echo "Invalid option: -$OPTARG" ;;
exit 1 -p)
;; shift
PORT="$1"
shift
;;
-c)
shift
GITHUB_PROXY_URL=$(ensure_trailing_slash "$1")
shift
;;
--auto-update)
AUTO_UPDATE_FLAG="true"
shift
;;
*)
echo "Invalid option: $1" >&2
exit 1
;;
esac esac
done done
@@ -63,7 +80,14 @@ if [ "$UNINSTALL" = "true" ]; then
# Remove the systemd service file # Remove the systemd service file
echo "Removing the systemd service file..." echo "Removing the systemd service file..."
rm /etc/systemd/system/beszel-hub.service rm -f /etc/systemd/system/beszel-hub.service
# Remove the update timer and service if they exist
echo "Removing the daily update service and timer..."
systemctl stop beszel-hub-update.timer 2>/dev/null
systemctl disable beszel-hub-update.timer 2>/dev/null
rm -f /etc/systemd/system/beszel-hub-update.service
rm -f /etc/systemd/system/beszel-hub-update.timer
# Reload the systemd daemon # Reload the systemd daemon
echo "Reloading the systemd daemon..." echo "Reloading the systemd daemon..."
@@ -75,7 +99,7 @@ if [ "$UNINSTALL" = "true" ]; then
# Remove the dedicated user # Remove the dedicated user
echo "Removing the dedicated user..." echo "Removing the dedicated user..."
userdel beszel userdel beszel 2>/dev/null
echo "The Beszel Hub has been uninstalled successfully!" echo "The Beszel Hub has been uninstalled successfully!"
exit 0 exit 0
@@ -151,4 +175,39 @@ if [ "$(systemctl is-active beszel-hub.service)" != "active" ]; then
exit 1 exit 1
fi fi
# Enable auto-update if flag is set to true
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
echo "Setting up daily automatic updates for beszel-hub..."
# Create systemd service for the daily update
cat >/etc/systemd/system/beszel-hub-update.service <<EOF
[Unit]
Description=Update beszel-hub if needed
Wants=beszel-hub.service
[Service]
Type=oneshot
ExecStart=/opt/beszel/beszel update
EOF
# Create systemd timer for the daily update
cat >/etc/systemd/system/beszel-hub-update.timer <<EOF
[Unit]
Description=Run beszel-hub update daily
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=4h
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now beszel-hub-update.timer
printf "\nDaily updates have been enabled.\n"
fi
echo "The Beszel Hub has been installed and configured successfully! It is now accessible on port $PORT." echo "The Beszel Hub has been installed and configured successfully! It is now accessible on port $PORT."