From b454b8d13039243acf3a4b4fdd6a301ab0559f0e Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:01:38 +0100 Subject: [PATCH 1/7] feat(packages): show all packages by default, add pagination --- backend/src/routes/packageRoutes.js | 18 +- frontend/src/pages/Packages.jsx | 296 ++++++++++++++++++++++------ 2 files changed, 244 insertions(+), 70 deletions(-) diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index e611bd3..e948526 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -67,7 +67,9 @@ router.get("/", async (req, res) => { latest_version: true, created_at: true, _count: { - host_packages: true, + select: { + host_packages: true, + }, }, }, skip, @@ -82,7 +84,7 @@ router.get("/", async (req, res) => { // Get additional stats for each package const packagesWithStats = await Promise.all( packages.map(async (pkg) => { - const [updatesCount, securityCount, affectedHosts] = await Promise.all([ + const [updatesCount, securityCount, packageHosts] = await Promise.all([ prisma.host_packages.count({ where: { package_id: pkg.id, @@ -117,17 +119,17 @@ router.get("/", async (req, res) => { return { ...pkg, - affectedHostsCount: pkg._count.hostPackages, - affectedHosts: affectedHosts.map((hp) => ({ - hostId: hp.host.id, - friendlyName: hp.host.friendly_name, - osType: hp.host.os_type, + packageHostsCount: pkg._count.host_packages, + packageHosts: packageHosts.map((hp) => ({ + hostId: hp.hosts.id, + friendlyName: hp.hosts.friendly_name, + osType: hp.hosts.os_type, currentVersion: hp.current_version, availableVersion: hp.available_version, isSecurityUpdate: hp.is_security_update, })), stats: { - totalInstalls: pkg._count.hostPackages, + totalInstalls: pkg._count.host_packages, updatesNeeded: updatesCount, securityUpdates: securityCount, }, diff --git a/frontend/src/pages/Packages.jsx b/frontend/src/pages/Packages.jsx index ebb088d..75ca544 100644 --- a/frontend/src/pages/Packages.jsx +++ b/frontend/src/pages/Packages.jsx @@ -4,6 +4,8 @@ import { ArrowDown, ArrowUp, ArrowUpDown, + ChevronLeft, + ChevronRight, Columns, Eye as EyeIcon, EyeOff as EyeOffIcon, @@ -17,16 +19,28 @@ import { } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { dashboardAPI } from "../utils/api"; +import { dashboardAPI, packagesAPI } from "../utils/api"; const Packages = () => { const [searchTerm, setSearchTerm] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); - const [securityFilter, setSecurityFilter] = useState("all"); + const [updateStatusFilter, setUpdateStatusFilter] = useState("all-packages"); const [hostFilter, setHostFilter] = useState("all"); const [sortField, setSortField] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [showColumnSettings, setShowColumnSettings] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(() => { + const saved = localStorage.getItem("packages-page-size"); + if (saved) { + const parsedSize = parseInt(saved, 10); + // Validate that the saved page size is one of the allowed values + if ([25, 50, 100, 200].includes(parsedSize)) { + return parsedSize; + } + } + return 25; // Default fallback + }); const [searchParams] = useSearchParams(); const navigate = useNavigate(); @@ -42,8 +56,8 @@ const Packages = () => { const [columnConfig, setColumnConfig] = useState(() => { const defaultConfig = [ { id: "name", label: "Package", visible: true, order: 0 }, - { id: "affectedHosts", label: "Affected Hosts", visible: true, order: 1 }, - { id: "priority", label: "Priority", visible: true, order: 2 }, + { id: "packageHosts", label: "Installed On", visible: true, order: 1 }, + { id: "status", label: "Status", visible: true, order: 2 }, { id: "latestVersion", label: "Latest Version", visible: true, order: 3 }, ]; @@ -65,10 +79,10 @@ const Packages = () => { localStorage.setItem("packages-column-config", JSON.stringify(newConfig)); }; - // Handle affected hosts click - const handleAffectedHostsClick = (pkg) => { - const affectedHosts = pkg.affectedHosts || []; - const hostIds = affectedHosts.map((host) => host.hostId); + // Handle hosts click (view hosts where package is installed) + const handlePackageHostsClick = (pkg) => { + const packageHosts = pkg.packageHosts || []; + const hostIds = packageHosts.map((host) => host.hostId); // Create URL with selected hosts and filter const params = new URLSearchParams(); @@ -86,27 +100,43 @@ const Packages = () => { // For outdated packages, we want to show all packages that need updates // This is the default behavior, so we don't need to change filters setCategoryFilter("all"); - setSecurityFilter("all"); + setUpdateStatusFilter("needs-updates"); } else if (filter === "security") { // For security updates, filter to show only security updates - setSecurityFilter("security"); + setUpdateStatusFilter("security-updates"); setCategoryFilter("all"); } }, [searchParams]); const { - data: packages, + data: packagesResponse, isLoading, error, refetch, isFetching, } = useQuery({ queryKey: ["packages"], - queryFn: () => dashboardAPI.getPackages().then((res) => res.data), + queryFn: () => packagesAPI.getAll({ limit: 1000 }).then((res) => res.data), staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes refetchOnWindowFocus: false, // Don't refetch when window regains focus }); + // Extract packages from the response and normalise the data structure + const packages = useMemo(() => { + if (!packagesResponse?.packages) return []; + + return packagesResponse.packages.map((pkg) => ({ + ...pkg, + // Normalise field names to match the frontend expectations + packageHostsCount: pkg.packageHostsCount || pkg.stats?.totalInstalls || 0, + latestVersion: pkg.latest_version || pkg.latestVersion || "Unknown", + isUpdatable: (pkg.stats?.updatesNeeded || 0) > 0, + isSecurityUpdate: (pkg.stats?.securityUpdates || 0) > 0, + // Ensure we have hosts array (for packages, this contains all hosts where the package is installed) + packageHosts: pkg.packageHosts || [], + })); + }, [packagesResponse]); + // Fetch hosts data to get total packages count const { data: hosts } = useQuery({ queryKey: ["hosts"], @@ -128,17 +158,30 @@ const Packages = () => { const matchesCategory = categoryFilter === "all" || pkg.category === categoryFilter; - const matchesSecurity = - securityFilter === "all" || - (securityFilter === "security" && pkg.isSecurityUpdate) || - (securityFilter === "regular" && !pkg.isSecurityUpdate); + const matchesUpdateStatus = + updateStatusFilter === "all-packages" || + updateStatusFilter === "needs-updates" || + (updateStatusFilter === "security-updates" && pkg.isSecurityUpdate) || + (updateStatusFilter === "regular-updates" && !pkg.isSecurityUpdate); - const affectedHosts = pkg.affectedHosts || []; + // For "all-packages", we don't filter by update status + // For other filters, we only show packages that need updates + const matchesUpdateNeeded = + updateStatusFilter === "all-packages" || + (pkg.stats?.updatesNeeded || 0) > 0; + + const packageHosts = pkg.packageHosts || []; const matchesHost = hostFilter === "all" || - affectedHosts.some((host) => host.hostId === hostFilter); + packageHosts.some((host) => host.hostId === hostFilter); - return matchesSearch && matchesCategory && matchesSecurity && matchesHost; + return ( + matchesSearch && + matchesCategory && + matchesUpdateStatus && + matchesUpdateNeeded && + matchesHost + ); }); // Sorting @@ -154,14 +197,38 @@ const Packages = () => { aValue = a.latestVersion?.toLowerCase() || ""; bValue = b.latestVersion?.toLowerCase() || ""; break; - case "affectedHosts": - aValue = a.affectedHostsCount || a.affectedHosts?.length || 0; - bValue = b.affectedHostsCount || b.affectedHosts?.length || 0; + case "packageHosts": + aValue = a.packageHostsCount || a.packageHosts?.length || 0; + bValue = b.packageHostsCount || b.packageHosts?.length || 0; break; - case "priority": - aValue = a.isSecurityUpdate ? 0 : 1; // Security updates first - bValue = b.isSecurityUpdate ? 0 : 1; + case "status": { + // Handle sorting for the three status states: Up to Date, Update Available, Security Update Available + const aNeedsUpdates = (a.stats?.updatesNeeded || 0) > 0; + const bNeedsUpdates = (b.stats?.updatesNeeded || 0) > 0; + + // Define priority order: Security Update (0) > Regular Update (1) > Up to Date (2) + let aPriority, bPriority; + + if (!aNeedsUpdates) { + aPriority = 2; // Up to Date + } else if (a.isSecurityUpdate) { + aPriority = 0; // Security Update + } else { + aPriority = 1; // Regular Update + } + + if (!bNeedsUpdates) { + bPriority = 2; // Up to Date + } else if (b.isSecurityUpdate) { + bPriority = 0; // Security Update + } else { + bPriority = 1; // Regular Update + } + + aValue = aPriority; + bValue = bPriority; break; + } default: aValue = a.name?.toLowerCase() || ""; bValue = b.name?.toLowerCase() || ""; @@ -177,12 +244,33 @@ const Packages = () => { packages, searchTerm, categoryFilter, - securityFilter, + updateStatusFilter, sortField, sortDirection, hostFilter, ]); + // Calculate pagination + const totalPages = Math.ceil(filteredAndSortedPackages.length / pageSize); + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedPackages = filteredAndSortedPackages.slice( + startIndex, + endIndex, + ); + + // Reset to first page when filters or page size change + // biome-ignore lint/correctness/useExhaustiveDependencies: We want this effect to run when filter values or page size change to reset pagination + useEffect(() => { + setCurrentPage(1); + }, [searchTerm, categoryFilter, updateStatusFilter, hostFilter, pageSize]); + + // Function to handle page size change and save to localStorage + const handlePageSizeChange = (newPageSize) => { + setPageSize(newPageSize); + localStorage.setItem("packages-page-size", newPageSize.toString()); + }; + // Get visible columns in order const visibleColumns = columnConfig .filter((col) => col.visible) @@ -231,8 +319,8 @@ const Packages = () => { const resetColumns = () => { const defaultConfig = [ { id: "name", label: "Package", visible: true, order: 0 }, - { id: "affectedHosts", label: "Affected Hosts", visible: true, order: 1 }, - { id: "priority", label: "Priority", visible: true, order: 2 }, + { id: "packageHosts", label: "Installed On", visible: true, order: 1 }, + { id: "status", label: "Status", visible: true, order: 2 }, { id: "latestVersion", label: "Latest Version", visible: true, order: 3 }, ]; updateColumnConfig(defaultConfig); @@ -262,31 +350,56 @@ const Packages = () => { ); - case "affectedHosts": { - const affectedHostsCount = - pkg.affectedHostsCount || pkg.affectedHosts?.length || 0; + case "packageHosts": { + // Show total number of hosts where this package is installed + const installedHostsCount = + pkg.packageHostsCount || + pkg.stats?.totalInstalls || + pkg.packageHosts?.length || + 0; + // For packages that need updates, show how many need updates + const hostsNeedingUpdates = pkg.stats?.updatesNeeded || 0; + + const displayText = + hostsNeedingUpdates > 0 && hostsNeedingUpdates < installedHostsCount + ? `${hostsNeedingUpdates}/${installedHostsCount} hosts` + : `${installedHostsCount} host${installedHostsCount !== 1 ? "s" : ""}`; + + const titleText = + hostsNeedingUpdates > 0 && hostsNeedingUpdates < installedHostsCount + ? `${hostsNeedingUpdates} of ${installedHostsCount} hosts need updates` + : `Installed on ${installedHostsCount} host${installedHostsCount !== 1 ? "s" : ""}`; + return ( ); } - case "priority": + case "status": { + // Check if this package needs updates + const needsUpdates = (pkg.stats?.updatesNeeded || 0) > 0; + + if (!needsUpdates) { + return Up to Date; + } + return pkg.isSecurityUpdate ? ( - Security Update + Security Update Available ) : ( - Regular Update + Update Available ); + } case "latestVersion": return (
{ const categories = [...new Set(packages?.map((pkg) => pkg.category).filter(Boolean))] || []; - // Calculate unique affected hosts - const uniqueAffectedHosts = new Set(); + // Calculate unique package hosts + const uniquePackageHosts = new Set(); packages?.forEach((pkg) => { - const affectedHosts = pkg.affectedHosts || []; - affectedHosts.forEach((host) => { - uniqueAffectedHosts.add(host.hostId); - }); + // Only count hosts for packages that need updates + if ((pkg.stats?.updatesNeeded || 0) > 0) { + const packageHosts = pkg.packageHosts || []; + packageHosts.forEach((host) => { + uniquePackageHosts.add(host.hostId); + }); + } }); - const uniqueAffectedHostsCount = uniqueAffectedHosts.size; + const uniquePackageHostsCount = uniquePackageHosts.size; - // Calculate total packages across all hosts (including up-to-date ones) - const totalPackagesCount = - hosts?.reduce((total, host) => { - return total + (host.totalPackagesCount || 0); - }, 0) || 0; + // Calculate total packages available + const totalPackagesCount = packages?.length || 0; - // Calculate outdated packages (packages that need updates) - const outdatedPackagesCount = packages?.length || 0; + // Calculate outdated packages + const outdatedPackagesCount = + packages?.filter((pkg) => (pkg.stats?.updatesNeeded || 0) > 0).length || 0; // Calculate security updates const securityUpdatesCount = - packages?.filter((pkg) => pkg.isSecurityUpdate).length || 0; + packages?.filter((pkg) => (pkg.stats?.securityUpdates || 0) > 0).length || + 0; if (isLoading) { return ( @@ -429,7 +544,7 @@ const Packages = () => { Hosts Pending Updates

- {uniqueAffectedHostsCount} + {uniquePackageHostsCount}

@@ -490,16 +605,21 @@ const Packages = () => { - {/* Security Filter */} + {/* Update Status Filter */}
@@ -539,12 +659,13 @@ const Packages = () => {

{packages?.length === 0 - ? "No packages need updates" + ? "No packages found" : "No packages match your filters"}

{packages?.length === 0 && (

- All packages are up to date across all hosts + Packages will appear here once hosts start reporting their + installed packages

)} @@ -571,7 +692,7 @@ const Packages = () => { - {filteredAndSortedPackages.map((pkg) => ( + {paginatedPackages.map((pkg) => ( { )} + + {/* Pagination Controls */} + {filteredAndSortedPackages.length > 0 && ( +
+
+
+ + Rows per page: + + +
+ + {startIndex + 1}- + {Math.min(endIndex, filteredAndSortedPackages.length)} of{" "} + {filteredAndSortedPackages.length} + +
+
+ + + Page {currentPage} of {totalPages} + + +
+
+ )} From 8bb16f08966fa666d938245456c041be0dd4f2e9 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:59:45 +0100 Subject: [PATCH 2/7] fix(api): update package host fields to match database schema --- backend/src/routes/packageRoutes.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index e948526..6a7a54b 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -162,19 +162,19 @@ router.get("/:packageId", async (req, res) => { include: { host_packages: { include: { - host: { + hosts: { select: { id: true, hostname: true, ip: true, - osType: true, - osVersion: true, - lastUpdate: true, + os_type: true, + os_version: true, + last_update: true, }, }, }, orderBy: { - needsUpdate: "desc", + needs_update: "desc", }, }, }, @@ -187,25 +187,25 @@ router.get("/:packageId", async (req, res) => { // Calculate statistics const stats = { totalInstalls: packageData.host_packages.length, - updatesNeeded: packageData.host_packages.filter((hp) => hp.needsUpdate) + updatesNeeded: packageData.host_packages.filter((hp) => hp.needs_update) .length, securityUpdates: packageData.host_packages.filter( - (hp) => hp.needsUpdate && hp.isSecurityUpdate, + (hp) => hp.needs_update && hp.is_security_update, ).length, - upToDate: packageData.host_packages.filter((hp) => !hp.needsUpdate) + upToDate: packageData.host_packages.filter((hp) => !hp.needs_update) .length, }; // Group by version const versionDistribution = packageData.host_packages.reduce((acc, hp) => { - const version = hp.currentVersion; + const version = hp.current_version; acc[version] = (acc[version] || 0) + 1; return acc; }, {}); // Group by OS type const osDistribution = packageData.host_packages.reduce((acc, hp) => { - const osType = hp.host.osType; + const osType = hp.hosts.os_type; acc[osType] = (acc[osType] || 0) + 1; return acc; }, {}); From 6f59a1981d0f35e1951d3a0b3e3d3a1831eef02d Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:01:45 +0100 Subject: [PATCH 3/7] feat(api): endpoint to retrieve hosts for a pkg With pagination and search functionality --- backend/src/routes/packageRoutes.js | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index 6a7a54b..4188b06 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -232,4 +232,109 @@ router.get("/:packageId", async (req, res) => { } }); +// Get hosts where a package is installed +router.get("/:packageId/hosts", async (req, res) => { + try { + const { packageId } = req.params; + const { + page = 1, + limit = 25, + search = "", + sortBy = "friendly_name", + sortOrder = "asc", + } = req.query; + + const offset = (parseInt(page, 10) - 1) * parseInt(limit, 10); + + // Build search conditions + const searchConditions = search + ? { + OR: [ + { + hosts: { + friendly_name: { contains: search, mode: "insensitive" }, + }, + }, + { hosts: { hostname: { contains: search, mode: "insensitive" } } }, + { current_version: { contains: search, mode: "insensitive" } }, + { available_version: { contains: search, mode: "insensitive" } }, + ], + } + : {}; + + // Build sort conditions + const orderBy = {}; + if ( + sortBy === "friendly_name" || + sortBy === "hostname" || + sortBy === "os_type" + ) { + orderBy.hosts = { [sortBy]: sortOrder }; + } else if (sortBy === "needs_update") { + orderBy[sortBy] = sortOrder; + } else { + orderBy[sortBy] = sortOrder; + } + + // Get total count + const totalCount = await prisma.host_packages.count({ + where: { + package_id: packageId, + ...searchConditions, + }, + }); + + // Get paginated results + const hostPackages = await prisma.host_packages.findMany({ + where: { + package_id: packageId, + ...searchConditions, + }, + include: { + hosts: { + select: { + id: true, + friendly_name: true, + hostname: true, + os_type: true, + os_version: true, + last_update: true, + }, + }, + }, + orderBy, + skip: offset, + take: parseInt(limit, 10), + }); + + // Transform the data for the frontend + const hosts = hostPackages.map((hp) => ({ + hostId: hp.hosts.id, + friendlyName: hp.hosts.friendly_name, + hostname: hp.hosts.hostname, + osType: hp.hosts.os_type, + osVersion: hp.hosts.os_version, + lastUpdate: hp.hosts.last_update, + currentVersion: hp.current_version, + availableVersion: hp.available_version, + needsUpdate: hp.needs_update, + isSecurityUpdate: hp.is_security_update, + lastChecked: hp.last_checked, + })); + + res.json({ + hosts, + pagination: { + page: parseInt(page, 10), + limit: parseInt(limit, 10), + total: totalCount, + pages: Math.ceil(totalCount / parseInt(limit, 10)), + }, + }); + } catch (error) { + console.error("Error fetching package hosts:", error); + res.status(500).json({ error: "Failed to fetch package hosts" }); + } +}); + module.exports = router; From fffc571453c31805e471631a0a1cf25f030b1d5b Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:02:15 +0100 Subject: [PATCH 4/7] feat(packages): complete package detail page Open by clicking package name --- frontend/src/pages/PackageDetail.jsx | 483 ++++++++++++++++++++++++++- frontend/src/pages/Packages.jsx | 14 +- 2 files changed, 478 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/PackageDetail.jsx b/frontend/src/pages/PackageDetail.jsx index a1183c7..8eb1abe 100644 --- a/frontend/src/pages/PackageDetail.jsx +++ b/frontend/src/pages/PackageDetail.jsx @@ -1,23 +1,478 @@ -import { Package } from "lucide-react"; -import { useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { + AlertTriangle, + ArrowLeft, + Calendar, + ChartColumnBig, + ChevronRight, + Download, + Package, + RefreshCw, + Search, + Server, + Shield, + Tag, +} from "lucide-react"; +import { useMemo, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { formatRelativeTime, packagesAPI } from "../utils/api"; const PackageDetail = () => { const { packageId } = useParams(); + const decodedPackageId = decodeURIComponent(packageId || ""); + const navigate = useNavigate(); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + + // Fetch package details + const { + data: packageData, + isLoading: isLoadingPackage, + error: packageError, + refetch: refetchPackage, + } = useQuery({ + queryKey: ["package", decodedPackageId], + queryFn: () => + packagesAPI.getById(decodedPackageId).then((res) => res.data), + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + enabled: !!decodedPackageId, + }); + + // Fetch hosts that have this package + const { + data: hostsData, + isLoading: isLoadingHosts, + error: hostsError, + refetch: refetchHosts, + } = useQuery({ + queryKey: ["package-hosts", decodedPackageId, searchTerm], + queryFn: () => + packagesAPI + .getHosts(decodedPackageId, { search: searchTerm, limit: 1000 }) + .then((res) => res.data), + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + enabled: !!decodedPackageId, + }); + + const hosts = hostsData?.hosts || []; + + // Filter and paginate hosts + const filteredAndPaginatedHosts = useMemo(() => { + let filtered = hosts; + + if (searchTerm) { + filtered = hosts.filter( + (host) => + host.friendly_name + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + host.hostname?.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return filtered.slice(startIndex, endIndex); + }, [hosts, searchTerm, currentPage, pageSize]); + + const totalPages = Math.ceil( + (searchTerm + ? hosts.filter( + (host) => + host.friendly_name + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + host.hostname?.toLowerCase().includes(searchTerm.toLowerCase()), + ).length + : hosts.length) / pageSize, + ); + + const handleHostClick = (hostId) => { + navigate(`/hosts/${hostId}`); + }; + + const handleRefresh = () => { + refetchPackage(); + refetchHosts(); + }; + + if (isLoadingPackage) { + return ( +
+ +
+ ); + } + + if (packageError) { + return ( +
+
+
+ +
+

+ Error loading package +

+

+ {packageError.message || "Failed to load package details"} +

+ +
+
+
+
+ ); + } + + if (!packageData) { + return ( +
+
+ +

+ Package not found +

+
+
+ ); + } + + const pkg = packageData; + const stats = packageData.stats || {}; return (
-
- -

- Package Details -

-

- Detailed view for package: {packageId} -

-

- This page will show package information, affected hosts, version - distribution, and more. -

+ {/* Header */} +
+
+ + +

+ {pkg.name} +

+
+ +
+ + {/* Package Overview */} +
+ {/* Main Package Info */} +
+
+
+ +
+

+ {pkg.name} +

+ {pkg.description && ( +

+ {pkg.description} +

+ )} +
+ {pkg.category && ( +
+ + + Category: {pkg.category} + +
+ )} + {pkg.latest_version && ( +
+ + + Latest: {pkg.latest_version} + +
+ )} + {pkg.updated_at && ( +
+ + + Updated: {formatRelativeTime(pkg.updated_at)} + +
+ )} +
+
+
+ + {/* Status Badge */} +
+ {stats.updatesNeeded > 0 ? ( + stats.securityUpdates > 0 ? ( + + + Security Update Available + + ) : ( + Update Available + ) + ) : ( + Up to Date + )} +
+
+
+ + {/* Statistics */} +
+
+
+ +

+ Installation Stats +

+
+
+
+ + Total Installations + + + {stats.totalInstalls || 0} + +
+ {stats.updatesNeeded > 0 && ( +
+ + Hosts Needing Updates + + + {stats.updatesNeeded} + +
+ )} + {stats.securityUpdates > 0 && ( +
+ + Security Updates + + + {stats.securityUpdates} + +
+ )} +
+ + Up to Date + + + {(stats.totalInstalls || 0) - (stats.updatesNeeded || 0)} + +
+
+
+
+
+ + {/* Hosts List */} +
+
+
+
+ +

+ Installed On Hosts ({hosts.length}) +

+
+
+ + {/* Search */} +
+ + { + setSearchTerm(e.target.value); + setCurrentPage(1); + }} + className="w-full pl-10 pr-4 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white placeholder-secondary-500 dark:placeholder-secondary-400" + /> +
+
+ +
+ {isLoadingHosts ? ( +
+ +
+ ) : hostsError ? ( +
+
+
+ +
+

+ Error loading hosts +

+

+ {hostsError.message || "Failed to load hosts"} +

+
+
+
+
+ ) : filteredAndPaginatedHosts.length === 0 ? ( +
+ +

+ {searchTerm + ? "No hosts match your search" + : "No hosts have this package installed"} +

+
+ ) : ( + <> + + + + + + + + + + + {filteredAndPaginatedHosts.map((host) => ( + handleHostClick(host.id)} + > + + + + + + ))} + +
+ Host + + Current Version + + Status + + Last Updated +
+
+ +
+
+ {host.friendly_name || host.hostname} +
+ {host.friendly_name && host.hostname && ( +
+ {host.hostname} +
+ )} +
+
+
+ {host.current_version || "Unknown"} + + {host.needs_update ? ( + host.is_security_update ? ( + + + Security Update + + ) : ( + + Update Available + + ) + ) : ( + + Up to Date + + )} + + {host.last_updated + ? formatRelativeTime(host.last_updated) + : "Never"} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ + Rows per page: + + +
+
+ + + Page {currentPage} of {totalPages} + + +
+
+ )} + + )} +
); diff --git a/frontend/src/pages/Packages.jsx b/frontend/src/pages/Packages.jsx index 75ca544..6cc3801 100644 --- a/frontend/src/pages/Packages.jsx +++ b/frontend/src/pages/Packages.jsx @@ -331,10 +331,14 @@ const Packages = () => { switch (column.id) { case "name": return ( -
- -
-
+
-
+ ); case "packageHosts": { // Show total number of hosts where this package is installed From 757feab9cd32b1bb58d2a314565080efa809b622 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:10:19 +0100 Subject: [PATCH 5/7] fix(packages): add needsUpdate and isSecurityUpdate fields to package hosts --- backend/src/routes/packageRoutes.js | 1 + frontend/src/pages/PackageDetail.jsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index 4188b06..ee2186f 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -126,6 +126,7 @@ router.get("/", async (req, res) => { osType: hp.hosts.os_type, currentVersion: hp.current_version, availableVersion: hp.available_version, + needsUpdate: hp.needs_update, isSecurityUpdate: hp.is_security_update, })), stats: { diff --git a/frontend/src/pages/PackageDetail.jsx b/frontend/src/pages/PackageDetail.jsx index 8eb1abe..06db020 100644 --- a/frontend/src/pages/PackageDetail.jsx +++ b/frontend/src/pages/PackageDetail.jsx @@ -400,8 +400,8 @@ const PackageDetail = () => { {host.current_version || "Unknown"} - {host.needs_update ? ( - host.is_security_update ? ( + {host.needsUpdate ? ( + host.isSecurityUpdate ? ( Security Update @@ -418,8 +418,8 @@ const PackageDetail = () => { )} - {host.last_updated - ? formatRelativeTime(host.last_updated) + {host.lastUpdate + ? formatRelativeTime(host.lastUpdate) : "Never"} From f085596b87f1b0ffa750637497ff156cf947fe1d Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:22:42 +0100 Subject: [PATCH 6/7] fix(packages): update host property names --- frontend/src/pages/PackageDetail.jsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/PackageDetail.jsx b/frontend/src/pages/PackageDetail.jsx index 06db020..6df6d17 100644 --- a/frontend/src/pages/PackageDetail.jsx +++ b/frontend/src/pages/PackageDetail.jsx @@ -66,9 +66,7 @@ const PackageDetail = () => { if (searchTerm) { filtered = hosts.filter( (host) => - host.friendly_name - ?.toLowerCase() - .includes(searchTerm.toLowerCase()) || + host.friendlyName?.toLowerCase().includes(searchTerm.toLowerCase()) || host.hostname?.toLowerCase().includes(searchTerm.toLowerCase()), ); } @@ -82,7 +80,7 @@ const PackageDetail = () => { (searchTerm ? hosts.filter( (host) => - host.friendly_name + host.friendlyName ?.toLowerCase() .includes(searchTerm.toLowerCase()) || host.hostname?.toLowerCase().includes(searchTerm.toLowerCase()), @@ -377,18 +375,18 @@ const PackageDetail = () => { {filteredAndPaginatedHosts.map((host) => ( handleHostClick(host.id)} + onClick={() => handleHostClick(host.hostId)} >
- {host.friendly_name || host.hostname} + {host.friendlyName || host.hostname}
- {host.friendly_name && host.hostname && ( + {host.friendlyName && host.hostname && (
{host.hostname}
@@ -397,7 +395,7 @@ const PackageDetail = () => {
- {host.current_version || "Unknown"} + {host.currentVersion || "Unknown"} {host.needsUpdate ? ( From 482a9e27c9b51af0d8e3090b9146627c8004a903 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:26:14 +0100 Subject: [PATCH 7/7] fix(packages): fix security update badge --- frontend/src/pages/Packages.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Packages.jsx b/frontend/src/pages/Packages.jsx index 6cc3801..ffac6bb 100644 --- a/frontend/src/pages/Packages.jsx +++ b/frontend/src/pages/Packages.jsx @@ -396,7 +396,7 @@ const Packages = () => { } return pkg.isSecurityUpdate ? ( - + Security Update Available