Added qty of connected and offline to the hosts dashboard page

This commit is contained in:
Muhammad Ibrahim
2025-10-15 22:40:52 +01:00
parent 5b77a1328d
commit c43afeb127
3 changed files with 98 additions and 18 deletions

View File

@@ -333,7 +333,7 @@ router.get("/overview", authenticateToken, async (_req, res) => {
{ {
name: "Collect Host Statistics", name: "Collect Host Statistics",
queue: QUEUE_NAMES.AGENT_COMMANDS, 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)`, schedule: `Every ${settings.update_interval} minutes (Agent-driven)`,
lastRun: recentJobs[3][0]?.finishedOn lastRun: recentJobs[3][0]?.finishedOn
? new Date(recentJobs[3][0].finishedOn).toLocaleString() ? new Date(recentJobs[3][0].finishedOn).toLocaleString()

View File

@@ -201,6 +201,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
agent_version: true, agent_version: true,
auto_update: true, auto_update: true,
notes: true, notes: true,
api_id: true,
host_groups: { host_groups: {
select: { select: {
id: true, id: true,

View File

@@ -21,6 +21,7 @@ import {
Square, Square,
Trash2, Trash2,
Users, Users,
Wifi,
X, X,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useId, useMemo, useState } from "react"; import { useEffect, useId, useMemo, useState } from "react";
@@ -403,6 +404,49 @@ const Hosts = () => {
// Track WebSocket status for all hosts // Track WebSocket status for all hosts
const [wsStatusMap, setWsStatusMap] = useState({}); 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 // Subscribe to WebSocket status changes for all hosts via SSE
useEffect(() => { useEffect(() => {
if (!hosts || hosts.length === 0) return; if (!hosts || hosts.length === 0) return;
@@ -425,7 +469,10 @@ const Hosts = () => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (isMounted) { if (isMounted) {
setWsStatusMap((prev) => ({ ...prev, [apiId]: data })); setWsStatusMap((prev) => {
const newMap = { ...prev, [apiId]: data };
return newMap;
});
} }
} catch (_err) { } catch (_err) {
// Silently handle parse errors // Silently handle parse errors
@@ -451,6 +498,7 @@ const Hosts = () => {
for (const host of hosts) { for (const host of hosts) {
if (host.api_id) { if (host.api_id) {
connectHost(host.api_id); connectHost(host.api_id);
} else {
} }
} }
@@ -628,7 +676,7 @@ const Hosts = () => {
osFilter === "all" || osFilter === "all" ||
host.os_type?.toLowerCase() === osFilter.toLowerCase(); 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 filter = searchParams.get("filter");
const matchesUrlFilter = const matchesUrlFilter =
(filter !== "needsUpdates" || (filter !== "needsUpdates" ||
@@ -636,7 +684,8 @@ const Hosts = () => {
(filter !== "inactive" || (filter !== "inactive" ||
(host.effectiveStatus || host.status) === "inactive") && (host.effectiveStatus || host.status) === "inactive") &&
(filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) && (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 // Hide stale filter
const matchesHideStale = !hideStale || !host.isStale; const matchesHideStale = !hideStale || !host.isStale;
@@ -721,6 +770,7 @@ const Hosts = () => {
sortDirection, sortDirection,
searchParams, searchParams,
hideStale, hideStale,
wsStatusMap,
]); ]);
// Get unique OS types from hosts for dynamic dropdown // Get unique OS types from hosts for dynamic dropdown
@@ -959,7 +1009,7 @@ const Hosts = () => {
<span <span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase ${ className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase ${
wsStatus.connected wsStatus.connected
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 shadow-lg shadow-green-500/50 dark:shadow-green-500/30 animate-pulse" ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 animate-pulse"
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200" : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
}`} }`}
title={ title={
@@ -1067,13 +1117,13 @@ const Hosts = () => {
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true }); navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
}; };
const handleStaleClick = () => { const handleConnectionStatusClick = () => {
// Filter to show stale/inactive hosts // Filter to show offline hosts (not connected via WebSocket)
setStatusFilter("inactive"); setStatusFilter("all");
setShowFilters(true); 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); const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.set("filter", "inactive"); newSearchParams.set("filter", "offline");
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true }); navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
}; };
@@ -1202,17 +1252,46 @@ const Hosts = () => {
<button <button
type="button" type="button"
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full" className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
onClick={handleStaleClick} onClick={handleConnectionStatusClick}
> >
<div className="flex items-center"> <div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-danger-600 mr-2" /> <Wifi className="h-5 w-5 text-primary-600 mr-2" />
<div> <div className="flex-1">
<p className="text-sm text-secondary-500 dark:text-white"> <p className="text-sm text-secondary-500 dark:text-white mb-1">
Stale Connection Status
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{hosts?.filter((h) => h.isStale).length || 0}
</p> </p>
{(() => {
const connectedCount =
hosts?.filter(
(h) => wsStatusMap[h.api_id]?.connected === true,
).length || 0;
const offlineCount =
hosts?.filter(
(h) => wsStatusMap[h.api_id]?.connected !== true,
).length || 0;
return (
<div className="flex gap-4">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{connectedCount}
</span>
<span className="text-xs text-secondary-500 dark:text-secondary-400">
Connected
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span className="text-sm font-medium text-secondary-900 dark:text-white">
{offlineCount}
</span>
<span className="text-xs text-secondary-500 dark:text-secondary-400">
Offline
</span>
</div>
</div>
);
})()}
</div> </div>
</div> </div>
</button> </button>