mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-31 12:03:47 +00:00 
			
		
		
		
	| @@ -231,13 +231,14 @@ detect_os() { | ||||
|             "opensuse"|"opensuse-leap"|"opensuse-tumbleweed") | ||||
|                 OS_TYPE="suse" | ||||
|                 ;; | ||||
|             "rocky"|"almalinux") | ||||
|             "almalinux") | ||||
|                 OS_TYPE="rhel" | ||||
|                 ;; | ||||
|             "ol") | ||||
|                 # Keep Oracle Linux as 'ol' for proper frontend identification | ||||
|                 OS_TYPE="ol" | ||||
|                 ;; | ||||
|             # Rocky Linux keeps its own identity for proper frontend display | ||||
|         esac | ||||
|          | ||||
|     elif [[ -f /etc/redhat-release ]]; then | ||||
| @@ -265,7 +266,7 @@ get_repository_info() { | ||||
|         "ubuntu"|"debian") | ||||
|             get_apt_repositories repos_json first | ||||
|             ;; | ||||
|         "centos"|"rhel"|"fedora"|"ol") | ||||
|         "centos"|"rhel"|"fedora"|"ol"|"rocky") | ||||
|             get_yum_repositories repos_json first | ||||
|             ;; | ||||
|         *) | ||||
| @@ -573,14 +574,118 @@ get_yum_repositories() { | ||||
|     local -n first_ref=$2 | ||||
|      | ||||
|     # Parse yum/dnf repository configuration | ||||
|     local repo_info="" | ||||
|     if command -v dnf >/dev/null 2>&1; then | ||||
|         local repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status") | ||||
|         repo_info=$(dnf repolist all --verbose 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status") | ||||
|     elif command -v yum >/dev/null 2>&1; then | ||||
|         local repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-name|^Repo-status") | ||||
|         repo_info=$(yum repolist all -v 2>/dev/null | grep -E "^Repo-id|^Repo-baseurl|^Repo-mirrors|^Repo-name|^Repo-status") | ||||
|     fi | ||||
|      | ||||
|     # This is a simplified implementation - would need more work for full YUM support | ||||
|     # For now, return empty for non-APT systems | ||||
|     if [[ -z "$repo_info" ]]; then | ||||
|         return | ||||
|     fi | ||||
|      | ||||
|     # Parse repository information | ||||
|     local current_repo="" | ||||
|     local repo_id="" | ||||
|     local repo_name="" | ||||
|     local repo_url="" | ||||
|     local repo_mirrors="" | ||||
|     local repo_status="" | ||||
|      | ||||
|     while IFS= read -r line; do | ||||
|         if [[ "$line" =~ ^Repo-id[[:space:]]+:[[:space:]]+(.+)$ ]]; then | ||||
|             # Process previous repository if we have one | ||||
|             if [[ -n "$current_repo" ]]; then | ||||
|                 process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status" | ||||
|             fi | ||||
|              | ||||
|             # Start new repository | ||||
|             repo_id="${BASH_REMATCH[1]}" | ||||
|             repo_name="$repo_id" | ||||
|             repo_url="" | ||||
|             repo_mirrors="" | ||||
|             repo_status="" | ||||
|             current_repo="$repo_id" | ||||
|              | ||||
|         elif [[ "$line" =~ ^Repo-name[[:space:]]+:[[:space:]]+(.+)$ ]]; then | ||||
|             repo_name="${BASH_REMATCH[1]}" | ||||
|              | ||||
|         elif [[ "$line" =~ ^Repo-baseurl[[:space:]]+:[[:space:]]+(.+)$ ]]; then | ||||
|             repo_url="${BASH_REMATCH[1]}" | ||||
|              | ||||
|         elif [[ "$line" =~ ^Repo-mirrors[[:space:]]+:[[:space:]]+(.+)$ ]]; then | ||||
|             repo_mirrors="${BASH_REMATCH[1]}" | ||||
|              | ||||
|         elif [[ "$line" =~ ^Repo-status[[:space:]]+:[[:space:]]+(.+)$ ]]; then | ||||
|             repo_status="${BASH_REMATCH[1]}" | ||||
|         fi | ||||
|     done <<< "$repo_info" | ||||
|      | ||||
|     # Process the last repository | ||||
|     if [[ -n "$current_repo" ]]; then | ||||
|         process_yum_repo repos_ref first_ref "$repo_id" "$repo_name" "$repo_url" "$repo_mirrors" "$repo_status" | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Process a single YUM repository and add it to the JSON | ||||
| process_yum_repo() { | ||||
|     local -n _repos_ref=$1 | ||||
|     local -n _first_ref=$2 | ||||
|     local repo_id="$3" | ||||
|     local repo_name="$4" | ||||
|     local repo_url="$5" | ||||
|     local repo_mirrors="$6" | ||||
|     local repo_status="$7" | ||||
|      | ||||
|     # Skip if we don't have essential info | ||||
|     if [[ -z "$repo_id" ]]; then | ||||
|         return | ||||
|     fi | ||||
|      | ||||
|     # Determine if repository is enabled | ||||
|     local is_enabled=false | ||||
|     if [[ "$repo_status" == "enabled" ]]; then | ||||
|         is_enabled=true | ||||
|     fi | ||||
|      | ||||
|     # Use baseurl if available, otherwise use mirrors URL | ||||
|     local final_url="" | ||||
|     if [[ -n "$repo_url" ]]; then | ||||
|         # Extract first URL if multiple are listed | ||||
|         final_url=$(echo "$repo_url" | head -n 1 | awk '{print $1}') | ||||
|     elif [[ -n "$repo_mirrors" ]]; then | ||||
|         final_url="$repo_mirrors" | ||||
|     fi | ||||
|      | ||||
|     # Skip if we don't have any URL | ||||
|     if [[ -z "$final_url" ]]; then | ||||
|         return | ||||
|     fi | ||||
|      | ||||
|     # Determine if repository uses HTTPS | ||||
|     local is_secure=false | ||||
|     if [[ "$final_url" =~ ^https:// ]]; then | ||||
|         is_secure=true | ||||
|     fi | ||||
|      | ||||
|     # Generate repository name if not provided | ||||
|     if [[ -z "$repo_name" ]]; then | ||||
|         repo_name="$repo_id" | ||||
|     fi | ||||
|      | ||||
|     # Clean up repository name and URL - escape quotes and backslashes | ||||
|     repo_name=$(echo "$repo_name" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') | ||||
|     final_url=$(echo "$final_url" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g') | ||||
|      | ||||
|     # Add to JSON | ||||
|     if [[ "$_first_ref" == true ]]; then | ||||
|         _first_ref=false | ||||
|     else | ||||
|         _repos_ref+="," | ||||
|     fi | ||||
|      | ||||
|     _repos_ref+="{\"name\":\"$repo_name\",\"url\":\"$final_url\",\"distribution\":\"$OS_VERSION\",\"components\":\"main\",\"repoType\":\"rpm\",\"isEnabled\":$is_enabled,\"isSecure\":$is_secure}" | ||||
| } | ||||
|  | ||||
| # Get package information based on OS | ||||
| @@ -592,7 +697,7 @@ get_package_info() { | ||||
|         "ubuntu"|"debian") | ||||
|             get_apt_packages packages_json first | ||||
|             ;; | ||||
|         "centos"|"rhel"|"fedora"|"ol") | ||||
|         "centos"|"rhel"|"fedora"|"ol"|"rocky") | ||||
|             get_yum_packages packages_json first | ||||
|             ;; | ||||
|         *) | ||||
|   | ||||
| @@ -929,31 +929,36 @@ const Layout = ({ children }) => { | ||||
| 					<div className="h-6 w-px bg-secondary-200 dark:bg-secondary-600 lg:hidden" /> | ||||
|  | ||||
| 					<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6"> | ||||
| 						{/* Page title - hidden on dashboard to give more space to search */} | ||||
| 						{location.pathname !== "/" && ( | ||||
| 							<div className="relative flex items-center"> | ||||
| 								<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap"> | ||||
| 									{getPageTitle()} | ||||
| 								</h2> | ||||
| 							</div> | ||||
| 						)} | ||||
| 						{/* Page title - hidden on dashboard, hosts, repositories, packages, and host details to give more space to search */} | ||||
| 						{!["/", "/hosts", "/repositories", "/packages"].includes( | ||||
| 							location.pathname, | ||||
| 						) && | ||||
| 							!location.pathname.startsWith("/hosts/") && ( | ||||
| 								<div className="relative flex items-center"> | ||||
| 									<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap"> | ||||
| 										{getPageTitle()} | ||||
| 									</h2> | ||||
| 								</div> | ||||
| 							)} | ||||
|  | ||||
| 						{/* Global Search Bar */} | ||||
| 						<div | ||||
| 							className={`flex items-center ${location.pathname === "/" ? "flex-1 max-w-none" : "max-w-sm"}`} | ||||
| 							className={`flex items-center ${["/", "/hosts", "/repositories", "/packages"].includes(location.pathname) || location.pathname.startsWith("/hosts/") ? "flex-1 max-w-none" : "max-w-sm"}`} | ||||
| 						> | ||||
| 							<GlobalSearch /> | ||||
| 						</div> | ||||
|  | ||||
| 						<div className="flex flex-1 items-center gap-x-4 lg:gap-x-6 justify-end"> | ||||
| 							{/* External Links */} | ||||
| 							<div className="hidden md:flex items-center gap-2"> | ||||
| 							<div className="hidden md:flex items-center gap-1"> | ||||
| 								{/* 1) GitHub */} | ||||
| 								<a | ||||
| 									href="https://github.com/PatchMon/PatchMon" | ||||
| 									target="_blank" | ||||
| 									rel="noopener noreferrer" | ||||
| 									className="flex items-center justify-center gap-1.5 px-3 py-2 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm group relative" | ||||
| 									title="GitHub" | ||||
| 									aria-label="GitHub" | ||||
| 								> | ||||
| 									<Github className="h-5 w-5 flex-shrink-0" /> | ||||
| 									{githubStars !== null && ( | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { | ||||
| 	Clock, | ||||
| 	Copy, | ||||
| 	Cpu, | ||||
| 	Database, | ||||
| 	Eye, | ||||
| 	EyeOff, | ||||
| 	HardDrive, | ||||
| @@ -31,6 +32,7 @@ import { | ||||
| 	dashboardAPI, | ||||
| 	formatDate, | ||||
| 	formatRelativeTime, | ||||
| 	repositoryAPI, | ||||
| 	settingsAPI, | ||||
| } from "../utils/api"; | ||||
| import { OSIcon } from "../utils/osIcons.jsx"; | ||||
| @@ -64,6 +66,15 @@ const HostDetail = () => { | ||||
| 		refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
| 	}); | ||||
|  | ||||
| 	// Fetch repository count for this host | ||||
| 	const { data: repositories, isLoading: isLoadingRepos } = useQuery({ | ||||
| 		queryKey: ["host-repositories", hostId], | ||||
| 		queryFn: () => repositoryAPI.getByHost(hostId).then((res) => res.data), | ||||
| 		staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer | ||||
| 		refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
| 		enabled: !!hostId, | ||||
| 	}); | ||||
|  | ||||
| 	// Tab change handler | ||||
| 	const handleTabChange = (tabName) => { | ||||
| 		setActiveTab(tabName); | ||||
| @@ -290,7 +301,7 @@ const HostDetail = () => { | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Package Statistics Cards */} | ||||
| 			<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6"> | ||||
| 			<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> | ||||
| 				<button | ||||
| 					type="button" | ||||
| 					onClick={() => navigate(`/packages?host=${hostId}`)} | ||||
| @@ -347,6 +358,25 @@ const HostDetail = () => { | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</button> | ||||
|  | ||||
| 				<button | ||||
| 					type="button" | ||||
| 					onClick={() => navigate(`/repositories?host=${hostId}`)} | ||||
| 					className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full" | ||||
| 					title="View repositories for this host" | ||||
| 				> | ||||
| 					<div className="flex items-center"> | ||||
| 						<Database className="h-5 w-5 text-blue-600 mr-2" /> | ||||
| 						<div> | ||||
| 							<p className="text-sm text-secondary-500 dark:text-white"> | ||||
| 								Repos | ||||
| 							</p> | ||||
| 							<p className="text-xl font-semibold text-secondary-900 dark:text-white"> | ||||
| 								{isLoadingRepos ? "..." : repositories?.length || 0} | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Main Content - Full Width */} | ||||
|   | ||||
| @@ -18,21 +18,31 @@ import { | ||||
| 	Unlock, | ||||
| 	X, | ||||
| } from "lucide-react"; | ||||
| import { useMemo, useState } from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { repositoryAPI } from "../utils/api"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| import { useNavigate, useSearchParams } from "react-router-dom"; | ||||
| import { dashboardAPI, repositoryAPI } from "../utils/api"; | ||||
|  | ||||
| const Repositories = () => { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	const navigate = useNavigate(); | ||||
| 	const [searchParams] = useSearchParams(); | ||||
| 	const [searchTerm, setSearchTerm] = useState(""); | ||||
| 	const [filterType, setFilterType] = useState("all"); // all, secure, insecure | ||||
| 	const [filterStatus, setFilterStatus] = useState("all"); // all, active, inactive | ||||
| 	const [hostFilter, setHostFilter] = useState(""); | ||||
| 	const [sortField, setSortField] = useState("name"); | ||||
| 	const [sortDirection, setSortDirection] = useState("asc"); | ||||
| 	const [showColumnSettings, setShowColumnSettings] = useState(false); | ||||
| 	const [deleteModalData, setDeleteModalData] = useState(null); | ||||
|  | ||||
| 	// Handle host filter from URL parameter | ||||
| 	useEffect(() => { | ||||
| 		const hostParam = searchParams.get("host"); | ||||
| 		if (hostParam) { | ||||
| 			setHostFilter(hostParam); | ||||
| 		} | ||||
| 	}, [searchParams]); | ||||
|  | ||||
| 	// Column configuration | ||||
| 	const [columnConfig, setColumnConfig] = useState(() => { | ||||
| 		const defaultConfig = [ | ||||
| @@ -82,6 +92,17 @@ const Repositories = () => { | ||||
| 		queryFn: () => repositoryAPI.getStats().then((res) => res.data), | ||||
| 	}); | ||||
|  | ||||
| 	// Fetch host information when filtering by host | ||||
| 	const { data: hosts } = useQuery({ | ||||
| 		queryKey: ["hosts"], | ||||
| 		queryFn: () => dashboardAPI.getHosts().then((res) => res.data), | ||||
| 		staleTime: 5 * 60 * 1000, | ||||
| 		enabled: !!hostFilter, | ||||
| 	}); | ||||
|  | ||||
| 	// Get the filtered host information | ||||
| 	const filteredHost = hosts?.find((host) => host.id === hostFilter); | ||||
|  | ||||
| 	// Delete repository mutation | ||||
| 	const deleteRepositoryMutation = useMutation({ | ||||
| 		mutationFn: (repositoryId) => repositoryAPI.delete(repositoryId), | ||||
| @@ -202,7 +223,11 @@ const Repositories = () => { | ||||
| 				(filterStatus === "active" && repo.is_active === true) || | ||||
| 				(filterStatus === "inactive" && repo.is_active === false); | ||||
|  | ||||
| 			return matchesSearch && matchesType && matchesStatus; | ||||
| 			// Filter by host if hostFilter is set | ||||
| 			const matchesHost = | ||||
| 				!hostFilter || repo.hosts?.some((host) => host.id === hostFilter); | ||||
|  | ||||
| 			return matchesSearch && matchesType && matchesStatus && matchesHost; | ||||
| 		}); | ||||
|  | ||||
| 		// Sort repositories | ||||
| @@ -237,6 +262,7 @@ const Repositories = () => { | ||||
| 		filterStatus, | ||||
| 		sortField, | ||||
| 		sortDirection, | ||||
| 		hostFilter, | ||||
| 	]); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| @@ -421,6 +447,31 @@ const Repositories = () => { | ||||
| 								</div> | ||||
| 							</div> | ||||
|  | ||||
| 							{/* Host Filter Indicator */} | ||||
| 							{hostFilter && filteredHost && ( | ||||
| 								<div className="flex items-center gap-2 px-3 py-2 bg-primary-50 dark:bg-primary-900 border border-primary-200 dark:border-primary-700 rounded-md"> | ||||
| 									<Server className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
| 									<span className="text-sm text-primary-700 dark:text-primary-300"> | ||||
| 										Filtered by: {filteredHost.friendly_name} | ||||
| 									</span> | ||||
| 									<button | ||||
| 										type="button" | ||||
| 										onClick={() => { | ||||
| 											setHostFilter(""); | ||||
| 											// Update URL to remove host parameter | ||||
| 											const newSearchParams = new URLSearchParams(searchParams); | ||||
| 											newSearchParams.delete("host"); | ||||
| 											navigate(`/repositories?${newSearchParams.toString()}`, { | ||||
| 												replace: true, | ||||
| 											}); | ||||
| 										}} | ||||
| 										className="text-primary-500 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-200" | ||||
| 									> | ||||
| 										<X className="h-4 w-4" /> | ||||
| 									</button> | ||||
| 								</div> | ||||
| 							)} | ||||
|  | ||||
| 							{/* Security Filter */} | ||||
| 							<div className="sm:w-48"> | ||||
| 								<select | ||||
|   | ||||
		Reference in New Issue
	
	Block a user