From c43afeb127960cfc562f9ffeacba8f1911a934d6 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Wed, 15 Oct 2025 22:40:52 +0100 Subject: [PATCH] Added qty of connected and offline to the hosts dashboard page --- backend/src/routes/automationRoutes.js | 2 +- backend/src/routes/dashboardRoutes.js | 1 + frontend/src/pages/Hosts.jsx | 113 +++++++++++++++++++++---- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/backend/src/routes/automationRoutes.js b/backend/src/routes/automationRoutes.js index a32c9a6..c5789ea 100644 --- a/backend/src/routes/automationRoutes.js +++ b/backend/src/routes/automationRoutes.js @@ -333,7 +333,7 @@ router.get("/overview", authenticateToken, async (_req, res) => { { name: "Collect Host Statistics", queue: QUEUE_NAMES.AGENT_COMMANDS, - description: "Collects package statistics from all connected agents", + description: "Collects package statistics from connected agents only", schedule: `Every ${settings.update_interval} minutes (Agent-driven)`, lastRun: recentJobs[3][0]?.finishedOn ? new Date(recentJobs[3][0].finishedOn).toLocaleString() diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js index 20d515f..73da744 100644 --- a/backend/src/routes/dashboardRoutes.js +++ b/backend/src/routes/dashboardRoutes.js @@ -201,6 +201,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => { agent_version: true, auto_update: true, notes: true, + api_id: true, host_groups: { select: { id: true, diff --git a/frontend/src/pages/Hosts.jsx b/frontend/src/pages/Hosts.jsx index 820396a..3621176 100644 --- a/frontend/src/pages/Hosts.jsx +++ b/frontend/src/pages/Hosts.jsx @@ -21,6 +21,7 @@ import { Square, Trash2, Users, + Wifi, X, } from "lucide-react"; import { useEffect, useId, useMemo, useState } from "react"; @@ -403,6 +404,49 @@ const Hosts = () => { // Track WebSocket status for all hosts const [wsStatusMap, setWsStatusMap] = useState({}); + // Fetch initial WebSocket status for all hosts + useEffect(() => { + if (!hosts || hosts.length === 0) return; + + const token = localStorage.getItem("token"); + if (!token) return; + + // Fetch initial WebSocket status for all hosts + const fetchInitialStatus = async () => { + const statusPromises = hosts + .filter((host) => host.api_id) + .map(async (host) => { + try { + const response = await fetch(`/api/v1/ws/status/${host.api_id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + if (response.ok) { + const data = await response.json(); + return { apiId: host.api_id, status: data.data }; + } + } catch (_error) { + // Silently handle errors + } + return { + apiId: host.api_id, + status: { connected: false, secure: false }, + }; + }); + + const results = await Promise.all(statusPromises); + const initialStatusMap = {}; + results.forEach(({ apiId, status }) => { + initialStatusMap[apiId] = status; + }); + + setWsStatusMap(initialStatusMap); + }; + + fetchInitialStatus(); + }, [hosts]); + // Subscribe to WebSocket status changes for all hosts via SSE useEffect(() => { if (!hosts || hosts.length === 0) return; @@ -425,7 +469,10 @@ const Hosts = () => { try { const data = JSON.parse(event.data); if (isMounted) { - setWsStatusMap((prev) => ({ ...prev, [apiId]: data })); + setWsStatusMap((prev) => { + const newMap = { ...prev, [apiId]: data }; + return newMap; + }); } } catch (_err) { // Silently handle parse errors @@ -451,6 +498,7 @@ const Hosts = () => { for (const host of hosts) { if (host.api_id) { connectHost(host.api_id); + } else { } } @@ -628,7 +676,7 @@ const Hosts = () => { osFilter === "all" || host.os_type?.toLowerCase() === osFilter.toLowerCase(); - // URL filter for hosts needing updates, inactive hosts, up-to-date hosts, or stale hosts + // URL filter for hosts needing updates, inactive hosts, up-to-date hosts, stale hosts, or offline hosts const filter = searchParams.get("filter"); const matchesUrlFilter = (filter !== "needsUpdates" || @@ -636,7 +684,8 @@ const Hosts = () => { (filter !== "inactive" || (host.effectiveStatus || host.status) === "inactive") && (filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) && - (filter !== "stale" || host.isStale); + (filter !== "stale" || host.isStale) && + (filter !== "offline" || wsStatusMap[host.api_id]?.connected !== true); // Hide stale filter const matchesHideStale = !hideStale || !host.isStale; @@ -721,6 +770,7 @@ const Hosts = () => { sortDirection, searchParams, hideStale, + wsStatusMap, ]); // Get unique OS types from hosts for dynamic dropdown @@ -959,7 +1009,7 @@ const Hosts = () => { { navigate(`/hosts?${newSearchParams.toString()}`, { replace: true }); }; - const handleStaleClick = () => { - // Filter to show stale/inactive hosts - setStatusFilter("inactive"); + const handleConnectionStatusClick = () => { + // Filter to show offline hosts (not connected via WebSocket) + setStatusFilter("all"); setShowFilters(true); - // We'll use the existing inactive URL filter logic + // Use a new URL filter for connection status const newSearchParams = new URLSearchParams(window.location.search); - newSearchParams.set("filter", "inactive"); + newSearchParams.set("filter", "offline"); navigate(`/hosts?${newSearchParams.toString()}`, { replace: true }); }; @@ -1202,17 +1252,46 @@ const Hosts = () => {