diff --git a/agent/connection_manager.go b/agent/connection_manager.go index 6bbc0a60..47e1779a 100644 --- a/agent/connection_manager.go +++ b/agent/connection_manager.go @@ -9,19 +9,21 @@ import ( "time" "github.com/henrygd/beszel/agent/health" + "github.com/henrygd/beszel/internal/entities/system" ) // ConnectionManager manages the connection state and events for the agent. // It handles both WebSocket and SSH connections, automatically switching between // them based on availability and managing reconnection attempts. type ConnectionManager struct { - agent *Agent // Reference to the parent agent - State ConnectionState // Current connection state - eventChan chan ConnectionEvent // Channel for connection events - wsClient *WebSocketClient // WebSocket client for hub communication - serverOptions ServerOptions // Configuration for SSH server - wsTicker *time.Ticker // Ticker for WebSocket connection attempts - isConnecting bool // Prevents multiple simultaneous reconnection attempts + agent *Agent // Reference to the parent agent + State ConnectionState // Current connection state + eventChan chan ConnectionEvent // Channel for connection events + wsClient *WebSocketClient // WebSocket client for hub communication + serverOptions ServerOptions // Configuration for SSH server + wsTicker *time.Ticker // Ticker for WebSocket connection attempts + isConnecting bool // Prevents multiple simultaneous reconnection attempts + ConnectionType system.ConnectionType } // ConnectionState represents the current connection state of the agent. @@ -144,15 +146,18 @@ func (c *ConnectionManager) handleStateChange(newState ConnectionState) { switch newState { case WebSocketConnected: slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host) + c.ConnectionType = system.ConnectionTypeWebSocket c.stopWsTicker() _ = c.agent.StopServer() c.isConnecting = false case SSHConnected: // stop new ws connection attempts slog.Info("SSH connection established") + c.ConnectionType = system.ConnectionTypeSSH c.stopWsTicker() c.isConnecting = false case Disconnected: + c.ConnectionType = system.ConnectionTypeNone if c.isConnecting { // Already handling reconnection, avoid duplicate attempts return diff --git a/agent/system.go b/agent/system.go index 6890e187..9ff28613 100644 --- a/agent/system.go +++ b/agent/system.go @@ -212,6 +212,7 @@ func (a *Agent) getSystemStats() system.Stats { } // update base system info + a.systemInfo.ConnectionType = a.connectionManager.ConnectionType a.systemInfo.Cpu = systemStats.Cpu a.systemInfo.LoadAvg = systemStats.LoadAvg // TODO: remove these in future release in favor of load avg array diff --git a/internal/entities/system/system.go b/internal/entities/system/system.go index ce43ebd7..47c2f0ab 100644 --- a/internal/entities/system/system.go +++ b/internal/entities/system/system.go @@ -84,6 +84,14 @@ const ( Freebsd ) +type ConnectionType = uint8 + +const ( + ConnectionTypeNone ConnectionType = iota + ConnectionTypeSSH + ConnectionTypeWebSocket +) + type Info struct { Hostname string `json:"h" cbor:"0,keyasint"` 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"` BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` // 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 diff --git a/internal/site/src/components/routes/system.tsx b/internal/site/src/components/routes/system.tsx index 202cdf35..345c7f57 100644 --- a/internal/site/src/components/routes/system.tsx +++ b/internal/site/src/components/routes/system.tsx @@ -3,7 +3,15 @@ import { Plural, Trans, useLingui } from "@lingui/react/macro" import { useStore } from "@nanostores/react" import { getPagePath } from "@nanostores/router" 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 React, { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react" 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 TemperatureChart from "@/components/charts/temperature-chart" 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 { $allSystemsByName, @@ -47,7 +55,7 @@ import { $router, navigate } from "../router" import Spinner from "../spinner" import { Button } from "../ui/button" 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select" import { Separator } from "../ui/separator" @@ -130,7 +138,7 @@ async function getStats( function dockerOrPodman(str: string, system: SystemRecord) { if (system.info.p) { - str = str.replace("docker", "podman").replace("Docker", "Podman") + return str.replace("docker", "podman").replace("Docker", "Podman") } return str } @@ -407,25 +415,45 @@ export default memo(function SystemDetail({ name }: { name: string }) {

{system.name}

-
- - {system.status === SystemStatus.Up && ( - + + + +
+ + {system.status === SystemStatus.Up && ( + + )} + + + {translatedStatus} +
+
+ {system.info.ct && ( + + {system.info.ct === ConnectionType.WebSocket ? ( +
+ WebSocket +
+ ) : ( +
+ SSH +
+ )} +
)} - -
- {translatedStatus} -
+ + + {systemInfo.map(({ value, label, Icon, hide }) => { if (hide || !value) { return null diff --git a/internal/site/src/components/systems-table/systems-table-columns.tsx b/internal/site/src/components/systems-table/systems-table-columns.tsx index 3797f70c..8fc7e586 100644 --- a/internal/site/src/components/systems-table/systems-table-columns.tsx +++ b/internal/site/src/components/systems-table/systems-table-columns.tsx @@ -6,6 +6,7 @@ import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-tabl import type { ClassValue } from "clsx" import { ArrowUpDownIcon, + ChevronRightSquareIcon, CopyIcon, CpuIcon, HardDriveIcon, @@ -20,7 +21,7 @@ import { } from "lucide-react" import { memo, useMemo, useRef, useState } from "react" 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 { cn, @@ -54,7 +55,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu" -import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons" +import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon, WebSocketIcon } from "../ui/icons" const STATUS_COLORS = { [SystemStatus.Up]: "bg-green-500", @@ -271,18 +272,18 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD return null } 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 ( - - +
+ {system.info.ct === ConnectionType.WebSocket && } + {system.info.ct === ConnectionType.SSH && } + {!system.info.ct && } {info.getValue() as string} - +
) }, }, diff --git a/internal/site/src/components/ui/icons.tsx b/internal/site/src/components/ui/icons.tsx index 96d2cc1b..edad2ab0 100644 --- a/internal/site/src/components/ui/icons.tsx +++ b/internal/site/src/components/ui/icons.tsx @@ -130,3 +130,12 @@ export function HourglassIcon(props: SVGProps) { ) } + +export function WebSocketIcon(props: SVGProps) { + return ( + + WebSocket + + + ) +} diff --git a/internal/site/src/lib/enums.ts b/internal/site/src/lib/enums.ts index 178c6b0c..bf0864f6 100644 --- a/internal/site/src/lib/enums.ts +++ b/internal/site/src/lib/enums.ts @@ -53,3 +53,9 @@ export enum HourFormat { "12h" = "12h", "24h" = "24h", } + +/** Connection type */ +export enum ConnectionType { + SSH = 1, + WebSocket, +} diff --git a/internal/site/src/types.d.ts b/internal/site/src/types.d.ts index 7f30bd80..5d8d25f7 100644 --- a/internal/site/src/types.d.ts +++ b/internal/site/src/types.d.ts @@ -1,5 +1,5 @@ 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 declare global { @@ -75,6 +75,8 @@ export interface SystemInfo { dt?: number /** operating system */ os?: Os + /** connection type */ + ct?: ConnectionType } export interface SystemStats {