diff --git a/internal/hub/systems/system_realtime.go b/internal/hub/systems/system_realtime.go
index 6c27d0bd..de74fd77 100644
--- a/internal/hub/systems/system_realtime.go
+++ b/internal/hub/systems/system_realtime.go
@@ -7,10 +7,21 @@ import (
"time"
"github.com/henrygd/beszel/internal/common"
+ "github.com/henrygd/beszel/internal/entities/container"
+ "github.com/henrygd/beszel/internal/entities/probe"
+ "github.com/henrygd/beszel/internal/entities/system"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/subscriptions"
)
+// realtimePayload wraps system data with optional network probe results for realtime broadcast.
+type realtimePayload struct {
+ Stats system.Stats `json:"stats"`
+ Info system.Info `json:"info"`
+ Containers []*container.Stats `json:"container"`
+ Probes map[string]probe.Result `json:"probes,omitempty"`
+}
+
type subscriptionInfo struct {
subscription string
connectedClients uint8
@@ -142,16 +153,25 @@ func (sm *SystemManager) startRealtimeWorker() {
// fetchRealtimeDataAndNotify fetches realtime data for all active subscriptions and notifies the clients.
func (sm *SystemManager) fetchRealtimeDataAndNotify() {
for systemId, info := range activeSubscriptions {
- system, err := sm.GetSystem(systemId)
+ sys, err := sm.GetSystem(systemId)
if err != nil {
continue
}
go func() {
- data, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
+ data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
if err != nil {
return
}
- bytes, err := json.Marshal(data)
+ payload := realtimePayload{
+ Stats: data.Stats,
+ Info: data.Info,
+ Containers: data.Containers,
+ }
+ // Fetch network probe results (lightweight in-memory read on agent)
+ if probes, err := sys.FetchNetworkProbeResults(); err == nil && len(probes) > 0 {
+ payload.Probes = probes
+ }
+ bytes, err := json.Marshal(payload)
if err == nil {
notify(sm.hub, info.subscription, bytes)
}
diff --git a/internal/site/src/components/routes/system.tsx b/internal/site/src/components/routes/system.tsx
index 4055899d..67cf2e22 100644
--- a/internal/site/src/components/routes/system.tsx
+++ b/internal/site/src/components/routes/system.tsx
@@ -29,6 +29,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
system,
systemStats,
containerData,
+ probeStats,
chartData,
containerChartConfigs,
details,
@@ -148,7 +149,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{hasSystemd && }
-
+
>
)
@@ -198,7 +199,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
{pageBottomExtraMargin > 0 &&
}
-
+
diff --git a/internal/site/src/components/routes/system/network-probes.tsx b/internal/site/src/components/routes/system/network-probes.tsx
index 74e2f78f..44c3ce85 100644
--- a/internal/site/src/components/routes/system/network-probes.tsx
+++ b/internal/site/src/components/routes/system/network-probes.tsx
@@ -23,10 +23,12 @@ export default function NetworkProbes({
systemId,
chartData,
grid,
+ realtimeProbeStats,
}: {
systemId: string
chartData: ChartData
grid: boolean
+ realtimeProbeStats?: NetworkProbeStatsRecord[]
}) {
const [probes, setProbes] = useState([])
const [stats, setStats] = useState([])
@@ -50,13 +52,42 @@ export default function NetworkProbes({
// Build set of current probe keys to filter out deleted probes from stats
const activeProbeKeys = useMemo(() => new Set(probes.map(probeKey)), [probes])
- // Fetch probe stats based on chart time
+ // Use realtime probe stats when in 1m mode
+ useEffect(() => {
+ if (chartTime !== "1m" || !realtimeProbeStats) {
+ return
+ }
+ // Filter stats to only include currently active probes
+ const data: NetworkProbeStatsRecord[] = realtimeProbeStats.map((r) => {
+ const filtered: NetworkProbeStatsRecord["stats"] = {}
+ for (const [key, val] of Object.entries(r.stats)) {
+ if (activeProbeKeys.has(key)) {
+ filtered[key] = val
+ }
+ }
+ return { stats: filtered, created: r.created }
+ })
+ setStats(data)
+ if (data.length > 0) {
+ const last = data[data.length - 1].stats
+ const latest: Record = {}
+ for (const [key, val] of Object.entries(last)) {
+ latest[key] = { avg: val.avg, loss: val.loss }
+ }
+ setLatestResults(latest)
+ }
+ }, [chartTime, realtimeProbeStats, activeProbeKeys])
+
+ // Fetch probe stats based on chart time (skip in realtime mode)
useEffect(() => {
if (probes.length === 0) {
setStats([])
setLatestResults({})
return
}
+ if (chartTime === "1m") {
+ return
+ }
const controller = new AbortController()
const statsType = chartTimeData[chartTime]?.type ?? "1m"
diff --git a/internal/site/src/components/routes/system/use-system-data.ts b/internal/site/src/components/routes/system/use-system-data.ts
index c83d2dff..c8f95834 100644
--- a/internal/site/src/components/routes/system/use-system-data.ts
+++ b/internal/site/src/components/routes/system/use-system-data.ts
@@ -19,6 +19,7 @@ import { chartTimeData, listen, parseSemVer, useBrowserStorage } from "@/lib/uti
import type {
ChartData,
ContainerStatsRecord,
+ NetworkProbeStatsRecord,
SystemDetailsRecord,
SystemInfo,
SystemRecord,
@@ -48,6 +49,7 @@ export function useSystemData(id: string) {
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [containerData, setContainerData] = useState([] as ChartData["containerData"])
+ const [probeStats, setProbeStats] = useState([] as NetworkProbeStatsRecord[])
const persistChartTime = useRef(false)
const statsRequestId = useRef(0)
const [chartLoading, setChartLoading] = useState(true)
@@ -119,24 +121,36 @@ export function useSystemData(id: string) {
pb.realtime
.subscribe(
`rt_metrics`,
- (data: { container: ContainerStatsRecord[]; info: SystemInfo; stats: SystemStats }) => {
+ (data: {
+ container: ContainerStatsRecord[]
+ info: SystemInfo
+ stats: SystemStats
+ probes?: NetworkProbeStatsRecord["stats"]
+ }) => {
const now = Date.now()
const statsPoint = { created: now, stats: data.stats } as SystemStatsRecord
const containerPoint =
data.container?.length > 0
? makeContainerPoint(now, data.container as unknown as ContainerStatsRecord["stats"])
: null
+ const probePoint: NetworkProbeStatsRecord | null = data.probes
+ ? { stats: data.probes, created: now }
+ : null
// on first message, make sure we clear out data from other time periods
if (isFirst) {
isFirst = false
setSystemStats([statsPoint])
setContainerData(containerPoint ? [containerPoint] : [])
+ setProbeStats(probePoint ? [probePoint] : [])
return
}
setSystemStats((prev) => appendData(prev, [statsPoint], 1000, 60))
if (containerPoint) {
setContainerData((prev) => appendData(prev, [containerPoint], 1000, 60))
}
+ if (probePoint) {
+ setProbeStats((prev) => appendData(prev, [probePoint], 1000, 60))
+ }
},
{ query: { system: system.id } }
)
@@ -322,6 +336,7 @@ export function useSystemData(id: string) {
system,
systemStats,
containerData,
+ probeStats,
chartData,
containerChartConfigs,
details,