mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-31 03:53:51 +00:00 
			
		
		
		
	Compare commits
	
		
			26 Commits
		
	
	
		
			renovate/v
			...
			renovate/m
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d8741ba083 | ||
|  | 1e75f2b1fe | ||
|  | 79317b0052 | ||
|  | 77a945a5b6 | ||
|  | 276d910e83 | ||
|  | dae536e96b | ||
|  | 8361caabe8 | ||
|  | f6d23e45b2 | ||
|  | aba0f5cb6b | ||
|  | 2ec2b3992c | ||
|  | f85721b292 | ||
|  | 1d2c003830 | ||
|  | 2975da0f69 | ||
|  | 93760d03e1 | ||
|  | 43fb54a683 | ||
|  | e9368d1a95 | ||
|  | 3ce8c02a31 | ||
|  | ac420901a6 | ||
|  | eb0218bdcb | ||
|  | 1f6f58360f | ||
|  | 746451c296 | ||
|  | 285e4c59ee | ||
|  | 9050595b7c | ||
|  | cc46940b0c | ||
|  | 203a065479 | ||
|  | 8864de6c15 | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1430,6 +1430,69 @@ router.patch( | |||||||
| 	}, | 	}, | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | // Force agent update for specific host | ||||||
|  | router.post( | ||||||
|  | 	"/:hostId/force-agent-update", | ||||||
|  | 	authenticateToken, | ||||||
|  | 	requireManageHosts, | ||||||
|  | 	async (req, res) => { | ||||||
|  | 		try { | ||||||
|  | 			const { hostId } = req.params; | ||||||
|  |  | ||||||
|  | 			// Get host to verify it exists | ||||||
|  | 			const host = await prisma.hosts.findUnique({ | ||||||
|  | 				where: { id: hostId }, | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			if (!host) { | ||||||
|  | 				return res.status(404).json({ error: "Host not found" }); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get queue manager | ||||||
|  | 			const { QUEUE_NAMES } = require("../services/automation"); | ||||||
|  | 			const queueManager = req.app.locals.queueManager; | ||||||
|  |  | ||||||
|  | 			if (!queueManager) { | ||||||
|  | 				return res.status(500).json({ | ||||||
|  | 					error: "Queue manager not available", | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get the agent-commands queue | ||||||
|  | 			const queue = queueManager.queues[QUEUE_NAMES.AGENT_COMMANDS]; | ||||||
|  |  | ||||||
|  | 			// Add job to queue | ||||||
|  | 			await queue.add( | ||||||
|  | 				"update_agent", | ||||||
|  | 				{ | ||||||
|  | 					api_id: host.api_id, | ||||||
|  | 					type: "update_agent", | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					attempts: 3, | ||||||
|  | 					backoff: { | ||||||
|  | 						type: "exponential", | ||||||
|  | 						delay: 2000, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			); | ||||||
|  |  | ||||||
|  | 			res.json({ | ||||||
|  | 				success: true, | ||||||
|  | 				message: "Agent update queued successfully", | ||||||
|  | 				host: { | ||||||
|  | 					id: host.id, | ||||||
|  | 					friendlyName: host.friendly_name, | ||||||
|  | 					apiId: host.api_id, | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 		} catch (error) { | ||||||
|  | 			console.error("Force agent update error:", error); | ||||||
|  | 			res.status(500).json({ error: "Failed to force agent update" }); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | ); | ||||||
|  |  | ||||||
| // Serve the installation script (requires API authentication) | // Serve the installation script (requires API authentication) | ||||||
| router.get("/install", async (req, res) => { | router.get("/install", async (req, res) => { | ||||||
| 	try { | 	try { | ||||||
|   | |||||||
| @@ -176,6 +176,15 @@ function pushSettingsUpdate(apiId, newInterval) { | |||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function pushUpdateAgent(apiId) { | ||||||
|  | 	const ws = apiIdToSocket.get(apiId); | ||||||
|  | 	safeSend(ws, JSON.stringify({ type: "update_agent" })); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getConnectionByApiId(apiId) { | ||||||
|  | 	return apiIdToSocket.get(apiId); | ||||||
|  | } | ||||||
|  |  | ||||||
| function pushUpdateNotification(apiId, updateInfo) { | function pushUpdateNotification(apiId, updateInfo) { | ||||||
| 	const ws = apiIdToSocket.get(apiId); | 	const ws = apiIdToSocket.get(apiId); | ||||||
| 	if (ws && ws.readyState === WebSocket.OPEN) { | 	if (ws && ws.readyState === WebSocket.OPEN) { | ||||||
| @@ -330,10 +339,12 @@ module.exports = { | |||||||
| 	broadcastSettingsUpdate, | 	broadcastSettingsUpdate, | ||||||
| 	pushReportNow, | 	pushReportNow, | ||||||
| 	pushSettingsUpdate, | 	pushSettingsUpdate, | ||||||
|  | 	pushUpdateAgent, | ||||||
| 	pushUpdateNotification, | 	pushUpdateNotification, | ||||||
| 	pushUpdateNotificationToAll, | 	pushUpdateNotificationToAll, | ||||||
| 	// Expose read-only view of connected agents | 	// Expose read-only view of connected agents | ||||||
| 	getConnectedApiIds: () => Array.from(apiIdToSocket.keys()), | 	getConnectedApiIds: () => Array.from(apiIdToSocket.keys()), | ||||||
|  | 	getConnectionByApiId, | ||||||
| 	isConnected: (apiId) => { | 	isConnected: (apiId) => { | ||||||
| 		const ws = apiIdToSocket.get(apiId); | 		const ws = apiIdToSocket.get(apiId); | ||||||
| 		return !!ws && ws.readyState === WebSocket.OPEN; | 		return !!ws && ws.readyState === WebSocket.OPEN; | ||||||
|   | |||||||
| @@ -190,6 +190,19 @@ class QueueManager { | |||||||
| 					// For settings update, we need additional data | 					// For settings update, we need additional data | ||||||
| 					const { update_interval } = job.data; | 					const { update_interval } = job.data; | ||||||
| 					agentWs.pushSettingsUpdate(api_id, update_interval); | 					agentWs.pushSettingsUpdate(api_id, update_interval); | ||||||
|  | 				} else if (type === "update_agent") { | ||||||
|  | 					// Force agent to update by sending WebSocket command | ||||||
|  | 					const ws = agentWs.getConnectionByApiId(api_id); | ||||||
|  | 					if (ws && ws.readyState === 1) { | ||||||
|  | 						// WebSocket.OPEN | ||||||
|  | 						agentWs.pushUpdateAgent(api_id); | ||||||
|  | 						console.log(`✅ Update command sent to agent ${api_id}`); | ||||||
|  | 					} else { | ||||||
|  | 						console.error(`❌ Agent ${api_id} is not connected`); | ||||||
|  | 						throw new Error( | ||||||
|  | 							`Agent ${api_id} is not connected. Cannot send update command.`, | ||||||
|  | 						); | ||||||
|  | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					console.error(`Unknown agent command type: ${type}`); | 					console.error(`Unknown agent command type: ${type}`); | ||||||
| 				} | 				} | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ WORKDIR /app/backend | |||||||
|  |  | ||||||
| RUN npm cache clean --force &&\ | RUN npm cache clean --force &&\ | ||||||
|     rm -rf node_modules ~/.npm /root/.npm &&\ |     rm -rf node_modules ~/.npm /root/.npm &&\ | ||||||
|     npm ci --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=0 &&\ |     npm ci --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=3 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 &&\ | ||||||
|     PRISMA_CLI_BINARY_TYPE=binary npm run db:generate &&\ |     PRISMA_CLI_BINARY_TYPE=binary npm run db:generate &&\ | ||||||
|     npm prune --omit=dev &&\ |     npm prune --omit=dev &&\ | ||||||
|     npm cache clean --force |     npm cache clean --force | ||||||
|   | |||||||
| @@ -21,9 +21,13 @@ WORKDIR /app/frontend | |||||||
|  |  | ||||||
| COPY frontend/package*.json ./ | COPY frontend/package*.json ./ | ||||||
|  |  | ||||||
| RUN npm cache clean --force &&\ | RUN echo "=== Starting npm install ===" &&\ | ||||||
|  |     npm cache clean --force &&\ | ||||||
|     rm -rf node_modules ~/.npm /root/.npm &&\ |     rm -rf node_modules ~/.npm /root/.npm &&\ | ||||||
|     npm install --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=0 |     echo "=== npm install ===" &&\ | ||||||
|  |     npm install --ignore-scripts --legacy-peer-deps --no-audit --prefer-online --fetch-retries=3 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 &&\ | ||||||
|  |     echo "=== npm install completed ===" &&\ | ||||||
|  |     npm cache clean --force | ||||||
|  |  | ||||||
| COPY frontend/ ./ | COPY frontend/ ./ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,8 +27,7 @@ | |||||||
| 		"react-chartjs-2": "^5.2.0", | 		"react-chartjs-2": "^5.2.0", | ||||||
| 		"react-dom": "^18.3.1", | 		"react-dom": "^18.3.1", | ||||||
| 		"react-icons": "^5.5.0", | 		"react-icons": "^5.5.0", | ||||||
| 		"react-router-dom": "^6.30.1", | 		"react-router-dom": "^7.0.0" | ||||||
| 		"trianglify": "^4.1.1" |  | ||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
| 		"@types/react": "^18.3.14", | 		"@types/react": "^18.3.14", | ||||||
|   | |||||||
| @@ -28,7 +28,6 @@ import { | |||||||
| import { useCallback, useEffect, useRef, useState } from "react"; | import { useCallback, useEffect, useRef, useState } from "react"; | ||||||
| import { FaReddit, FaYoutube } from "react-icons/fa"; | import { FaReddit, FaYoutube } from "react-icons/fa"; | ||||||
| import { Link, useLocation, useNavigate } from "react-router-dom"; | import { Link, useLocation, useNavigate } from "react-router-dom"; | ||||||
| import trianglify from "trianglify"; |  | ||||||
| import { useAuth } from "../contexts/AuthContext"; | import { useAuth } from "../contexts/AuthContext"; | ||||||
| import { useColorTheme } from "../contexts/ColorThemeContext"; | import { useColorTheme } from "../contexts/ColorThemeContext"; | ||||||
| import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; | import { useUpdateNotification } from "../contexts/UpdateNotificationContext"; | ||||||
| @@ -237,31 +236,93 @@ const Layout = ({ children }) => { | |||||||
| 		navigate("/hosts?action=add"); | 		navigate("/hosts?action=add"); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	// Generate Trianglify background for dark mode | 	// Generate clean radial gradient background with subtle triangular accents for dark mode | ||||||
| 	useEffect(() => { | 	useEffect(() => { | ||||||
| 		const generateBackground = () => { | 		const generateBackground = () => { | ||||||
| 			if ( | 			if ( | ||||||
| 				bgCanvasRef.current && | 				!bgCanvasRef.current || | ||||||
| 				themeConfig?.login && | 				!themeConfig?.login || | ||||||
| 				document.documentElement.classList.contains("dark") | 				!document.documentElement.classList.contains("dark") | ||||||
| 			) { | 			) { | ||||||
| 				// Get current date as seed for daily variation | 				return; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			const canvas = bgCanvasRef.current; | ||||||
|  | 			canvas.width = window.innerWidth; | ||||||
|  | 			canvas.height = window.innerHeight; | ||||||
|  | 			const ctx = canvas.getContext("2d"); | ||||||
|  |  | ||||||
|  | 			// Get theme colors - pick first color from each palette | ||||||
|  | 			const xColors = themeConfig.login.xColors || [ | ||||||
|  | 				"#667eea", | ||||||
|  | 				"#764ba2", | ||||||
|  | 				"#f093fb", | ||||||
|  | 				"#4facfe", | ||||||
|  | 			]; | ||||||
|  | 			const yColors = themeConfig.login.yColors || [ | ||||||
|  | 				"#667eea", | ||||||
|  | 				"#764ba2", | ||||||
|  | 				"#f093fb", | ||||||
|  | 				"#4facfe", | ||||||
|  | 			]; | ||||||
|  |  | ||||||
|  | 			// Use date for daily color rotation | ||||||
| 			const today = new Date(); | 			const today = new Date(); | ||||||
| 				const dateSeed = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; | 			const seed = | ||||||
|  | 				today.getFullYear() * 10000 + today.getMonth() * 100 + today.getDate(); | ||||||
|  | 			const random = (s) => { | ||||||
|  | 				const x = Math.sin(s) * 10000; | ||||||
|  | 				return x - Math.floor(x); | ||||||
|  | 			}; | ||||||
|  |  | ||||||
| 				// Generate pattern with selected theme configuration | 			const color1 = xColors[Math.floor(random(seed) * xColors.length)]; | ||||||
| 				const pattern = trianglify({ | 			const color2 = yColors[Math.floor(random(seed + 1000) * yColors.length)]; | ||||||
| 					width: window.innerWidth, |  | ||||||
| 					height: window.innerHeight, |  | ||||||
| 					cellSize: themeConfig.login.cellSize, |  | ||||||
| 					variance: themeConfig.login.variance, |  | ||||||
| 					seed: dateSeed, |  | ||||||
| 					xColors: themeConfig.login.xColors, |  | ||||||
| 					yColors: themeConfig.login.yColors, |  | ||||||
| 				}); |  | ||||||
|  |  | ||||||
| 				// Render to canvas | 			// Create clean radial gradient from center to bottom-right corner | ||||||
| 				pattern.toCanvas(bgCanvasRef.current); | 			const gradient = ctx.createRadialGradient( | ||||||
|  | 				canvas.width * 0.3, // Center slightly left | ||||||
|  | 				canvas.height * 0.3, // Center slightly up | ||||||
|  | 				0, | ||||||
|  | 				canvas.width * 0.5, // Expand to cover screen | ||||||
|  | 				canvas.height * 0.5, | ||||||
|  | 				Math.max(canvas.width, canvas.height) * 1.2, | ||||||
|  | 			); | ||||||
|  |  | ||||||
|  | 			// Subtle gradient with darker corners | ||||||
|  | 			gradient.addColorStop(0, color1); | ||||||
|  | 			gradient.addColorStop(0.6, color2); | ||||||
|  | 			gradient.addColorStop(1, "#0a0a0a"); // Very dark edges | ||||||
|  |  | ||||||
|  | 			ctx.fillStyle = gradient; | ||||||
|  | 			ctx.fillRect(0, 0, canvas.width, canvas.height); | ||||||
|  |  | ||||||
|  | 			// Add subtle triangular shapes as accents across entire background | ||||||
|  | 			const cellSize = 180; | ||||||
|  | 			const cols = Math.ceil(canvas.width / cellSize) + 1; | ||||||
|  | 			const rows = Math.ceil(canvas.height / cellSize) + 1; | ||||||
|  |  | ||||||
|  | 			for (let y = 0; y < rows; y++) { | ||||||
|  | 				for (let x = 0; x < cols; x++) { | ||||||
|  | 					const idx = y * cols + x; | ||||||
|  | 					// Draw more triangles (less sparse) | ||||||
|  | 					if (random(seed + idx + 5000) > 0.4) { | ||||||
|  | 						const baseX = | ||||||
|  | 							x * cellSize + random(seed + idx * 3) * cellSize * 0.8; | ||||||
|  | 						const baseY = | ||||||
|  | 							y * cellSize + random(seed + idx * 3 + 100) * cellSize * 0.8; | ||||||
|  | 						const size = 50 + random(seed + idx * 4) * 100; | ||||||
|  |  | ||||||
|  | 						ctx.beginPath(); | ||||||
|  | 						ctx.moveTo(baseX, baseY); | ||||||
|  | 						ctx.lineTo(baseX + size, baseY); | ||||||
|  | 						ctx.lineTo(baseX + size / 2, baseY - size * 0.866); | ||||||
|  | 						ctx.closePath(); | ||||||
|  |  | ||||||
|  | 						// More visible white with slightly higher opacity | ||||||
|  | 						ctx.fillStyle = `rgba(255, 255, 255, ${0.05 + random(seed + idx * 5) * 0.08})`; | ||||||
|  | 						ctx.fill(); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -187,6 +187,16 @@ const HostDetail = () => { | |||||||
| 		}, | 		}, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	// Force agent update mutation | ||||||
|  | 	const forceAgentUpdateMutation = useMutation({ | ||||||
|  | 		mutationFn: () => | ||||||
|  | 			adminHostsAPI.forceAgentUpdate(hostId).then((res) => res.data), | ||||||
|  | 		onSuccess: () => { | ||||||
|  | 			queryClient.invalidateQueries(["host", hostId]); | ||||||
|  | 			queryClient.invalidateQueries(["hosts"]); | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  |  | ||||||
| 	const updateFriendlyNameMutation = useMutation({ | 	const updateFriendlyNameMutation = useMutation({ | ||||||
| 		mutationFn: (friendlyName) => | 		mutationFn: (friendlyName) => | ||||||
| 			adminHostsAPI | 			adminHostsAPI | ||||||
| @@ -703,6 +713,29 @@ const HostDetail = () => { | |||||||
| 											/> | 											/> | ||||||
| 										</button> | 										</button> | ||||||
| 									</div> | 									</div> | ||||||
|  |  | ||||||
|  | 									<div> | ||||||
|  | 										<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1.5"> | ||||||
|  | 											Force Update | ||||||
|  | 										</p> | ||||||
|  | 										<button | ||||||
|  | 											type="button" | ||||||
|  | 											onClick={() => forceAgentUpdateMutation.mutate()} | ||||||
|  | 											disabled={forceAgentUpdateMutation.isPending} | ||||||
|  | 											className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-md hover:bg-primary-100 dark:hover:bg-primary-900/40 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | ||||||
|  | 										> | ||||||
|  | 											<RefreshCw | ||||||
|  | 												className={`h-3 w-3 ${ | ||||||
|  | 													forceAgentUpdateMutation.isPending | ||||||
|  | 														? "animate-spin" | ||||||
|  | 														: "" | ||||||
|  | 												}`} | ||||||
|  | 											/> | ||||||
|  | 											{forceAgentUpdateMutation.isPending | ||||||
|  | 												? "Updating..." | ||||||
|  | 												: "Update Now"} | ||||||
|  | 										</button> | ||||||
|  | 									</div> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						)} | 						)} | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ import { useEffect, useId, useRef, useState } from "react"; | |||||||
| import { FaReddit, FaYoutube } from "react-icons/fa"; | import { FaReddit, FaYoutube } from "react-icons/fa"; | ||||||
|  |  | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import trianglify from "trianglify"; |  | ||||||
| import DiscordIcon from "../components/DiscordIcon"; | import DiscordIcon from "../components/DiscordIcon"; | ||||||
| import { useAuth } from "../contexts/AuthContext"; | import { useAuth } from "../contexts/AuthContext"; | ||||||
| import { useColorTheme } from "../contexts/ColorThemeContext"; | import { useColorTheme } from "../contexts/ColorThemeContext"; | ||||||
| @@ -57,27 +56,87 @@ const Login = () => { | |||||||
|  |  | ||||||
| 	const navigate = useNavigate(); | 	const navigate = useNavigate(); | ||||||
|  |  | ||||||
| 	// Generate Trianglify background based on selected theme | 	// Generate clean radial gradient background with subtle triangular accents | ||||||
| 	useEffect(() => { | 	useEffect(() => { | ||||||
| 		const generateBackground = () => { | 		const generateBackground = () => { | ||||||
| 			if (canvasRef.current && themeConfig?.login) { | 			if (!canvasRef.current || !themeConfig?.login) return; | ||||||
| 				// Get current date as seed for daily variation |  | ||||||
|  | 			const canvas = canvasRef.current; | ||||||
|  | 			canvas.width = canvas.offsetWidth; | ||||||
|  | 			canvas.height = canvas.offsetHeight; | ||||||
|  | 			const ctx = canvas.getContext("2d"); | ||||||
|  |  | ||||||
|  | 			// Get theme colors - pick first color from each palette | ||||||
|  | 			const xColors = themeConfig.login.xColors || [ | ||||||
|  | 				"#667eea", | ||||||
|  | 				"#764ba2", | ||||||
|  | 				"#f093fb", | ||||||
|  | 				"#4facfe", | ||||||
|  | 			]; | ||||||
|  | 			const yColors = themeConfig.login.yColors || [ | ||||||
|  | 				"#667eea", | ||||||
|  | 				"#764ba2", | ||||||
|  | 				"#f093fb", | ||||||
|  | 				"#4facfe", | ||||||
|  | 			]; | ||||||
|  |  | ||||||
|  | 			// Use date for daily color rotation | ||||||
| 			const today = new Date(); | 			const today = new Date(); | ||||||
| 				const dateSeed = `${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; | 			const seed = | ||||||
|  | 				today.getFullYear() * 10000 + today.getMonth() * 100 + today.getDate(); | ||||||
|  | 			const random = (s) => { | ||||||
|  | 				const x = Math.sin(s) * 10000; | ||||||
|  | 				return x - Math.floor(x); | ||||||
|  | 			}; | ||||||
|  |  | ||||||
| 				// Generate pattern with selected theme configuration | 			const color1 = xColors[Math.floor(random(seed) * xColors.length)]; | ||||||
| 				const pattern = trianglify({ | 			const color2 = yColors[Math.floor(random(seed + 1000) * yColors.length)]; | ||||||
| 					width: canvasRef.current.offsetWidth, |  | ||||||
| 					height: canvasRef.current.offsetHeight, |  | ||||||
| 					cellSize: themeConfig.login.cellSize, |  | ||||||
| 					variance: themeConfig.login.variance, |  | ||||||
| 					seed: dateSeed, |  | ||||||
| 					xColors: themeConfig.login.xColors, |  | ||||||
| 					yColors: themeConfig.login.yColors, |  | ||||||
| 				}); |  | ||||||
|  |  | ||||||
| 				// Render to canvas | 			// Create clean radial gradient from center to bottom-right corner | ||||||
| 				pattern.toCanvas(canvasRef.current); | 			const gradient = ctx.createRadialGradient( | ||||||
|  | 				canvas.width * 0.3, // Center slightly left | ||||||
|  | 				canvas.height * 0.3, // Center slightly up | ||||||
|  | 				0, | ||||||
|  | 				canvas.width * 0.5, // Expand to cover screen | ||||||
|  | 				canvas.height * 0.5, | ||||||
|  | 				Math.max(canvas.width, canvas.height) * 1.2, | ||||||
|  | 			); | ||||||
|  |  | ||||||
|  | 			// Subtle gradient with darker corners | ||||||
|  | 			gradient.addColorStop(0, color1); | ||||||
|  | 			gradient.addColorStop(0.6, color2); | ||||||
|  | 			gradient.addColorStop(1, "#0a0a0a"); // Very dark edges | ||||||
|  |  | ||||||
|  | 			ctx.fillStyle = gradient; | ||||||
|  | 			ctx.fillRect(0, 0, canvas.width, canvas.height); | ||||||
|  |  | ||||||
|  | 			// Add subtle triangular shapes as accents across entire background | ||||||
|  | 			const cellSize = 180; | ||||||
|  | 			const cols = Math.ceil(canvas.width / cellSize) + 1; | ||||||
|  | 			const rows = Math.ceil(canvas.height / cellSize) + 1; | ||||||
|  |  | ||||||
|  | 			for (let y = 0; y < rows; y++) { | ||||||
|  | 				for (let x = 0; x < cols; x++) { | ||||||
|  | 					const idx = y * cols + x; | ||||||
|  | 					// Draw more triangles (less sparse) | ||||||
|  | 					if (random(seed + idx + 5000) > 0.4) { | ||||||
|  | 						const baseX = | ||||||
|  | 							x * cellSize + random(seed + idx * 3) * cellSize * 0.8; | ||||||
|  | 						const baseY = | ||||||
|  | 							y * cellSize + random(seed + idx * 3 + 100) * cellSize * 0.8; | ||||||
|  | 						const size = 50 + random(seed + idx * 4) * 100; | ||||||
|  |  | ||||||
|  | 						ctx.beginPath(); | ||||||
|  | 						ctx.moveTo(baseX, baseY); | ||||||
|  | 						ctx.lineTo(baseX + size, baseY); | ||||||
|  | 						ctx.lineTo(baseX + size / 2, baseY - size * 0.866); | ||||||
|  | 						ctx.closePath(); | ||||||
|  |  | ||||||
|  | 						// More visible white with slightly higher opacity | ||||||
|  | 						ctx.fillStyle = `rgba(255, 255, 255, ${0.05 + random(seed + idx * 5) * 0.08})`; | ||||||
|  | 						ctx.fill(); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| @@ -90,7 +149,7 @@ const Login = () => { | |||||||
|  |  | ||||||
| 		window.addEventListener("resize", handleResize); | 		window.addEventListener("resize", handleResize); | ||||||
| 		return () => window.removeEventListener("resize", handleResize); | 		return () => window.removeEventListener("resize", handleResize); | ||||||
| 	}, [themeConfig]); // Regenerate when theme changes | 	}, [themeConfig]); | ||||||
|  |  | ||||||
| 	// Check if signup is enabled | 	// Check if signup is enabled | ||||||
| 	useEffect(() => { | 	useEffect(() => { | ||||||
|   | |||||||
| @@ -95,6 +95,7 @@ export const adminHostsAPI = { | |||||||
| 		api.put("/hosts/bulk/groups", { hostIds, groupIds }), | 		api.put("/hosts/bulk/groups", { hostIds, groupIds }), | ||||||
| 	toggleAutoUpdate: (hostId, autoUpdate) => | 	toggleAutoUpdate: (hostId, autoUpdate) => | ||||||
| 		api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }), | 		api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }), | ||||||
|  | 	forceAgentUpdate: (hostId) => api.post(`/hosts/${hostId}/force-agent-update`), | ||||||
| 	updateFriendlyName: (hostId, friendlyName) => | 	updateFriendlyName: (hostId, friendlyName) => | ||||||
| 		api.patch(`/hosts/${hostId}/friendly-name`, { | 		api.patch(`/hosts/${hostId}/friendly-name`, { | ||||||
| 			friendly_name: friendlyName, | 			friendly_name: friendlyName, | ||||||
|   | |||||||
							
								
								
									
										739
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										739
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										90
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								setup.sh
									
									
									
									
									
								
							| @@ -1797,7 +1797,12 @@ create_agent_version() { | |||||||
|         cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/" |         cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/" | ||||||
|          |          | ||||||
|         print_status "Agent version management removed - using file-based approach" |         print_status "Agent version management removed - using file-based approach" | ||||||
| # Ensure we close the conditional and the function properly |     fi | ||||||
|  |      | ||||||
|  |     # Make agent binaries executable | ||||||
|  |     if [ -d "$APP_DIR/agents" ]; then | ||||||
|  |         chmod +x "$APP_DIR/agents/patchmon-agent-linux-"* 2>/dev/null || true | ||||||
|  |         print_status "Agent binaries made executable" | ||||||
|     fi |     fi | ||||||
|  |  | ||||||
|     return 0 |     return 0 | ||||||
| @@ -2703,6 +2708,13 @@ update_env_file() { | |||||||
|     : ${TFA_MAX_REMEMBER_SESSIONS:=5} |     : ${TFA_MAX_REMEMBER_SESSIONS:=5} | ||||||
|     : ${TFA_SUSPICIOUS_ACTIVITY_THRESHOLD:=3} |     : ${TFA_SUSPICIOUS_ACTIVITY_THRESHOLD:=3} | ||||||
|      |      | ||||||
|  |     # Prisma Connection Pool | ||||||
|  |     : ${DB_CONNECTION_LIMIT:=30} | ||||||
|  |     : ${DB_POOL_TIMEOUT:=20} | ||||||
|  |     : ${DB_CONNECT_TIMEOUT:=10} | ||||||
|  |     : ${DB_IDLE_TIMEOUT:=300} | ||||||
|  |     : ${DB_MAX_LIFETIME:=1800} | ||||||
|  |      | ||||||
|     # Track which variables were added |     # Track which variables were added | ||||||
|     local added_vars=() |     local added_vars=() | ||||||
|      |      | ||||||
| @@ -2764,6 +2776,21 @@ update_env_file() { | |||||||
|     if ! grep -q "^TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=" "$env_file"; then |     if ! grep -q "^TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=" "$env_file"; then | ||||||
|         added_vars+=("TFA_SUSPICIOUS_ACTIVITY_THRESHOLD") |         added_vars+=("TFA_SUSPICIOUS_ACTIVITY_THRESHOLD") | ||||||
|     fi |     fi | ||||||
|  |     if ! grep -q "^DB_CONNECTION_LIMIT=" "$env_file"; then | ||||||
|  |         added_vars+=("DB_CONNECTION_LIMIT") | ||||||
|  |     fi | ||||||
|  |     if ! grep -q "^DB_POOL_TIMEOUT=" "$env_file"; then | ||||||
|  |         added_vars+=("DB_POOL_TIMEOUT") | ||||||
|  |     fi | ||||||
|  |     if ! grep -q "^DB_CONNECT_TIMEOUT=" "$env_file"; then | ||||||
|  |         added_vars+=("DB_CONNECT_TIMEOUT") | ||||||
|  |     fi | ||||||
|  |     if ! grep -q "^DB_IDLE_TIMEOUT=" "$env_file"; then | ||||||
|  |         added_vars+=("DB_IDLE_TIMEOUT") | ||||||
|  |     fi | ||||||
|  |     if ! grep -q "^DB_MAX_LIFETIME=" "$env_file"; then | ||||||
|  |         added_vars+=("DB_MAX_LIFETIME") | ||||||
|  |     fi | ||||||
|      |      | ||||||
|     # If there are missing variables, add them |     # If there are missing variables, add them | ||||||
|     if [ ${#added_vars[@]} -gt 0 ]; then |     if [ ${#added_vars[@]} -gt 0 ]; then | ||||||
| @@ -2849,6 +2876,25 @@ EOF | |||||||
|             echo "TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=$TFA_SUSPICIOUS_ACTIVITY_THRESHOLD" >> "$env_file" |             echo "TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=$TFA_SUSPICIOUS_ACTIVITY_THRESHOLD" >> "$env_file" | ||||||
|         fi |         fi | ||||||
|          |          | ||||||
|  |         # Add Prisma connection pool config if missing | ||||||
|  |         if printf '%s\n' "${added_vars[@]}" | grep -q "DB_CONNECTION_LIMIT"; then | ||||||
|  |             echo "" >> "$env_file" | ||||||
|  |             echo "# Database Connection Pool Configuration (Prisma)" >> "$env_file" | ||||||
|  |             echo "DB_CONNECTION_LIMIT=$DB_CONNECTION_LIMIT" >> "$env_file" | ||||||
|  |         fi | ||||||
|  |         if printf '%s\n' "${added_vars[@]}" | grep -q "DB_POOL_TIMEOUT"; then | ||||||
|  |             echo "DB_POOL_TIMEOUT=$DB_POOL_TIMEOUT" >> "$env_file" | ||||||
|  |         fi | ||||||
|  |         if printf '%s\n' "${added_vars[@]}" | grep -q "DB_CONNECT_TIMEOUT"; then | ||||||
|  |             echo "DB_CONNECT_TIMEOUT=$DB_CONNECT_TIMEOUT" >> "$env_file" | ||||||
|  |         fi | ||||||
|  |         if printf '%s\n' "${added_vars[@]}" | grep -q "DB_IDLE_TIMEOUT"; then | ||||||
|  |             echo "DB_IDLE_TIMEOUT=$DB_IDLE_TIMEOUT" >> "$env_file" | ||||||
|  |         fi | ||||||
|  |         if printf '%s\n' "${added_vars[@]}" | grep -q "DB_MAX_LIFETIME"; then | ||||||
|  |             echo "DB_MAX_LIFETIME=$DB_MAX_LIFETIME" >> "$env_file" | ||||||
|  |         fi | ||||||
|  |          | ||||||
|         print_status ".env file updated with ${#added_vars[@]} new variable(s)" |         print_status ".env file updated with ${#added_vars[@]} new variable(s)" | ||||||
|         print_info "Added variables: ${added_vars[*]}" |         print_info "Added variables: ${added_vars[*]}" | ||||||
|     else |     else | ||||||
| @@ -2918,11 +2964,37 @@ update_installation() { | |||||||
|     print_info "Installation directory: $instance_dir" |     print_info "Installation directory: $instance_dir" | ||||||
|     print_info "Service name: $service_name" |     print_info "Service name: $service_name" | ||||||
|      |      | ||||||
|     # Verify it's a git repository |     # Verify it's a git repository, if not, initialize it | ||||||
|     if [ ! -d "$instance_dir/.git" ]; then |     if [ ! -d "$instance_dir/.git" ]; then | ||||||
|         print_error "Installation directory is not a git repository" |         print_warning "Installation directory is not a git repository" | ||||||
|         print_error "Cannot perform git-based update" |         print_info "Attempting to re-initialize as git repository..." | ||||||
|         exit 1 |          | ||||||
|  |         cd "$instance_dir" || exit 1 | ||||||
|  |          | ||||||
|  |         # Initialize git repository | ||||||
|  |         git init | ||||||
|  |         git remote add origin https://github.com/PatchMon/PatchMon.git | ||||||
|  |          | ||||||
|  |         # Fetch all branches | ||||||
|  |         git fetch origin | ||||||
|  |          | ||||||
|  |         # Try to determine current version from package.json or default to main | ||||||
|  |         local current_branch="main" | ||||||
|  |         if [ -f "$instance_dir/backend/package.json" ]; then | ||||||
|  |             local pkg_version=$(grep '"version"' "$instance_dir/backend/package.json" | head -1 | sed 's/.*"version": "\(.*\)".*/\1/') | ||||||
|  |             if [ -n "$pkg_version" ]; then | ||||||
|  |                 # Check if there's a release branch for this version | ||||||
|  |                 if git ls-remote --heads origin | grep -q "release/$(echo $pkg_version | sed 's/\./-/g')"; then | ||||||
|  |                     current_branch="release/$(echo $pkg_version | sed 's/\./-/g')" | ||||||
|  |                 fi | ||||||
|  |             fi | ||||||
|  |         fi | ||||||
|  |          | ||||||
|  |         # Reset to the determined branch | ||||||
|  |         git reset --hard "origin/$current_branch" | ||||||
|  |         git checkout -B "$current_branch" "origin/$current_branch" | ||||||
|  |          | ||||||
|  |         print_success "Repository initialized successfully" | ||||||
|     fi |     fi | ||||||
|      |      | ||||||
|     # Add git safe.directory to avoid ownership issues when running as root |     # Add git safe.directory to avoid ownership issues when running as root | ||||||
| @@ -2931,6 +3003,8 @@ update_installation() { | |||||||
|      |      | ||||||
|     # Load existing .env to get database credentials |     # Load existing .env to get database credentials | ||||||
|     if [ -f "$instance_dir/backend/.env" ]; then |     if [ -f "$instance_dir/backend/.env" ]; then | ||||||
|  |         # Unset color variables before sourcing to prevent ANSI escape sequences from leaking into .env | ||||||
|  |         unset RED GREEN YELLOW BLUE NC | ||||||
|         source "$instance_dir/backend/.env" |         source "$instance_dir/backend/.env" | ||||||
|         print_status "Loaded existing configuration" |         print_status "Loaded existing configuration" | ||||||
|          |          | ||||||
| @@ -3026,6 +3100,12 @@ update_installation() { | |||||||
|     print_info "Building frontend..." |     print_info "Building frontend..." | ||||||
|     npm run build |     npm run build | ||||||
|      |      | ||||||
|  |     # Make agent binaries executable | ||||||
|  |     if [ -d "$instance_dir/agents" ]; then | ||||||
|  |         chmod +x "$instance_dir/agents/patchmon-agent-linux-"* 2>/dev/null || true | ||||||
|  |         print_status "Agent binaries made executable" | ||||||
|  |     fi | ||||||
|  |      | ||||||
|     # Run database migrations with self-healing |     # Run database migrations with self-healing | ||||||
|     print_info "Running database migrations..." |     print_info "Running database migrations..." | ||||||
|     cd "$instance_dir/backend" |     cd "$instance_dir/backend" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user