mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 10:46:16 +01:00
display agent connection type in hub (ssh, websocket)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -212,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,3 +53,9 @@ export enum HourFormat {
|
|||||||
"12h" = "12h",
|
"12h" = "12h",
|
||||||
"24h" = "24h",
|
"24h" = "24h",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Connection type */
|
||||||
|
export enum ConnectionType {
|
||||||
|
SSH = 1,
|
||||||
|
WebSocket,
|
||||||
|
}
|
||||||
|
|||||||
4
internal/site/src/types.d.ts
vendored
4
internal/site/src/types.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user