mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			renovate/e
			...
			post1-3-2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e57ff7612e | ||
| 
						 | 
					7a3d98862f | ||
| 
						 | 
					913976b7f6 | ||
| 
						 | 
					53ff3bb1e2 | 
@@ -24,7 +24,7 @@
 | 
			
		||||
		"cors": "^2.8.5",
 | 
			
		||||
		"dotenv": "^16.4.7",
 | 
			
		||||
		"express": "^4.21.2",
 | 
			
		||||
		"express-rate-limit": "^8.0.0",
 | 
			
		||||
		"express-rate-limit": "^7.5.0",
 | 
			
		||||
		"express-validator": "^7.2.0",
 | 
			
		||||
		"helmet": "^8.0.0",
 | 
			
		||||
		"ioredis": "^5.8.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -860,6 +860,9 @@ router.post(
 | 
			
		||||
					last_login: user.last_login,
 | 
			
		||||
					created_at: user.created_at,
 | 
			
		||||
					updated_at: user.updated_at,
 | 
			
		||||
					// Include user preferences so they're available immediately after login
 | 
			
		||||
					theme_preference: user.theme_preference,
 | 
			
		||||
					color_theme: user.color_theme,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
@@ -952,10 +955,24 @@ router.post(
 | 
			
		||||
				return res.status(401).json({ error: "Invalid verification code" });
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Update last login
 | 
			
		||||
			await prisma.users.update({
 | 
			
		||||
			// Update last login and fetch complete user data
 | 
			
		||||
			const updatedUser = await prisma.users.update({
 | 
			
		||||
				where: { id: user.id },
 | 
			
		||||
				data: { last_login: new Date() },
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true,
 | 
			
		||||
					username: true,
 | 
			
		||||
					email: true,
 | 
			
		||||
					first_name: true,
 | 
			
		||||
					last_name: true,
 | 
			
		||||
					role: true,
 | 
			
		||||
					is_active: true,
 | 
			
		||||
					last_login: true,
 | 
			
		||||
					created_at: true,
 | 
			
		||||
					updated_at: true,
 | 
			
		||||
					theme_preference: true,
 | 
			
		||||
					color_theme: true,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Create session with access and refresh tokens
 | 
			
		||||
@@ -975,14 +992,7 @@ router.post(
 | 
			
		||||
				refresh_token: session.refresh_token,
 | 
			
		||||
				expires_at: session.expires_at,
 | 
			
		||||
				tfa_bypass_until: session.tfa_bypass_until,
 | 
			
		||||
				user: {
 | 
			
		||||
					id: user.id,
 | 
			
		||||
					username: user.username,
 | 
			
		||||
					email: user.email,
 | 
			
		||||
					first_name: user.first_name,
 | 
			
		||||
					last_name: user.last_name,
 | 
			
		||||
					role: user.role,
 | 
			
		||||
				},
 | 
			
		||||
				user: updatedUser,
 | 
			
		||||
			});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error("TFA verification error:", error);
 | 
			
		||||
@@ -1014,13 +1024,27 @@ router.put(
 | 
			
		||||
			.withMessage("Username must be at least 3 characters"),
 | 
			
		||||
		body("email").optional().isEmail().withMessage("Valid email is required"),
 | 
			
		||||
		body("first_name")
 | 
			
		||||
			.optional()
 | 
			
		||||
			.isLength({ min: 1 })
 | 
			
		||||
			.withMessage("First name must be at least 1 character"),
 | 
			
		||||
			.optional({ nullable: true, checkFalsy: true })
 | 
			
		||||
			.custom((value) => {
 | 
			
		||||
				// Allow null, undefined, or empty string to clear the field
 | 
			
		||||
				if (value === null || value === undefined || value === "") {
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
				// If provided, must be at least 1 character after trimming
 | 
			
		||||
				return typeof value === "string" && value.trim().length >= 1;
 | 
			
		||||
			})
 | 
			
		||||
			.withMessage("First name must be at least 1 character if provided"),
 | 
			
		||||
		body("last_name")
 | 
			
		||||
			.optional()
 | 
			
		||||
			.isLength({ min: 1 })
 | 
			
		||||
			.withMessage("Last name must be at least 1 character"),
 | 
			
		||||
			.optional({ nullable: true, checkFalsy: true })
 | 
			
		||||
			.custom((value) => {
 | 
			
		||||
				// Allow null, undefined, or empty string to clear the field
 | 
			
		||||
				if (value === null || value === undefined || value === "") {
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
				// If provided, must be at least 1 character after trimming
 | 
			
		||||
				return typeof value === "string" && value.trim().length >= 1;
 | 
			
		||||
			})
 | 
			
		||||
			.withMessage("Last name must be at least 1 character if provided"),
 | 
			
		||||
	],
 | 
			
		||||
	async (req, res) => {
 | 
			
		||||
		try {
 | 
			
		||||
@@ -1034,16 +1058,22 @@ router.put(
 | 
			
		||||
				updated_at: new Date(),
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			if (username) updateData.username = username;
 | 
			
		||||
			if (email) updateData.email = email;
 | 
			
		||||
			// Handle first_name and last_name - allow empty strings to clear the field
 | 
			
		||||
			// Handle all fields consistently - trim and update if provided
 | 
			
		||||
			if (username) updateData.username = username.trim();
 | 
			
		||||
			if (email) updateData.email = email.trim();
 | 
			
		||||
			if (first_name !== undefined) {
 | 
			
		||||
				// Allow null or empty string to clear the field, otherwise trim
 | 
			
		||||
				updateData.first_name =
 | 
			
		||||
					first_name === "" ? null : first_name.trim() || null;
 | 
			
		||||
					first_name === "" || first_name === null
 | 
			
		||||
						? null
 | 
			
		||||
						: first_name.trim() || null;
 | 
			
		||||
			}
 | 
			
		||||
			if (last_name !== undefined) {
 | 
			
		||||
				// Allow null or empty string to clear the field, otherwise trim
 | 
			
		||||
				updateData.last_name =
 | 
			
		||||
					last_name === "" ? null : last_name.trim() || null;
 | 
			
		||||
					last_name === "" || last_name === null
 | 
			
		||||
						? null
 | 
			
		||||
						: last_name.trim() || null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Check if username/email already exists (excluding current user)
 | 
			
		||||
@@ -1106,16 +1136,6 @@ router.put(
 | 
			
		||||
			// Use fresh data if available, otherwise fallback to updatedUser
 | 
			
		||||
			const responseUser = freshUser || updatedUser;
 | 
			
		||||
 | 
			
		||||
			// Log update for debugging (only log in non-production)
 | 
			
		||||
			if (process.env.NODE_ENV !== "production") {
 | 
			
		||||
				console.log("Profile updated:", {
 | 
			
		||||
					userId: req.user.id,
 | 
			
		||||
					first_name: responseUser.first_name,
 | 
			
		||||
					last_name: responseUser.last_name,
 | 
			
		||||
					updated_at: responseUser.updated_at,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			res.json({
 | 
			
		||||
				message: "Profile updated successfully",
 | 
			
		||||
				user: responseUser,
 | 
			
		||||
 
 | 
			
		||||
@@ -450,8 +450,8 @@ function AppRoutes() {
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
	return (
 | 
			
		||||
		<ThemeProvider>
 | 
			
		||||
		<AuthProvider>
 | 
			
		||||
			<ThemeProvider>
 | 
			
		||||
				<SettingsProvider>
 | 
			
		||||
					<ColorThemeProvider>
 | 
			
		||||
						<UpdateNotificationProvider>
 | 
			
		||||
@@ -461,8 +461,8 @@ function App() {
 | 
			
		||||
						</UpdateNotificationProvider>
 | 
			
		||||
					</ColorThemeProvider>
 | 
			
		||||
				</SettingsProvider>
 | 
			
		||||
			</AuthProvider>
 | 
			
		||||
			</ThemeProvider>
 | 
			
		||||
		</AuthProvider>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -138,6 +138,9 @@ export const AuthProvider = ({ children }) => {
 | 
			
		||||
					setPermissions(userPermissions);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Note: User preferences will be automatically fetched by ColorThemeContext
 | 
			
		||||
				// when the component mounts, so no need to invalidate here
 | 
			
		||||
 | 
			
		||||
				return { success: true };
 | 
			
		||||
			} else {
 | 
			
		||||
				// Handle HTTP error responses (like 500 CORS errors)
 | 
			
		||||
@@ -224,8 +227,6 @@ export const AuthProvider = ({ children }) => {
 | 
			
		||||
			const data = await response.json();
 | 
			
		||||
 | 
			
		||||
			if (response.ok) {
 | 
			
		||||
				console.log("Profile updated - received user data:", data.user);
 | 
			
		||||
 | 
			
		||||
				// Validate that we received user data with expected fields
 | 
			
		||||
				if (!data.user || !data.user.id) {
 | 
			
		||||
					console.error("Invalid user data in response:", data);
 | 
			
		||||
@@ -239,15 +240,6 @@ export const AuthProvider = ({ children }) => {
 | 
			
		||||
				setUser(data.user);
 | 
			
		||||
				localStorage.setItem("user", JSON.stringify(data.user));
 | 
			
		||||
 | 
			
		||||
				// Log update for debugging (only in non-production)
 | 
			
		||||
				if (process.env.NODE_ENV !== "production") {
 | 
			
		||||
					console.log("User data updated in localStorage:", {
 | 
			
		||||
						id: data.user.id,
 | 
			
		||||
						first_name: data.user.first_name,
 | 
			
		||||
						last_name: data.user.last_name,
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return { success: true, user: data.user };
 | 
			
		||||
			} else {
 | 
			
		||||
				// Handle HTTP error responses (like 500 CORS errors)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,15 @@
 | 
			
		||||
import { useQuery } from "@tanstack/react-query";
 | 
			
		||||
import { createContext, useContext, useEffect, useState } from "react";
 | 
			
		||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import {
 | 
			
		||||
	createContext,
 | 
			
		||||
	useCallback,
 | 
			
		||||
	useContext,
 | 
			
		||||
	useEffect,
 | 
			
		||||
	useMemo,
 | 
			
		||||
	useRef,
 | 
			
		||||
	useState,
 | 
			
		||||
} from "react";
 | 
			
		||||
import { userPreferencesAPI } from "../utils/api";
 | 
			
		||||
import { useAuth } from "./AuthContext";
 | 
			
		||||
 | 
			
		||||
const ColorThemeContext = createContext();
 | 
			
		||||
 | 
			
		||||
@@ -123,48 +132,108 @@ export const THEME_PRESETS = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ColorThemeProvider = ({ children }) => {
 | 
			
		||||
	const [colorTheme, setColorTheme] = useState(() => {
 | 
			
		||||
		// Initialize from localStorage for immediate render
 | 
			
		||||
		return localStorage.getItem("colorTheme") || "cyber_blue";
 | 
			
		||||
	});
 | 
			
		||||
	const [isLoading, setIsLoading] = useState(true);
 | 
			
		||||
	const queryClient = useQueryClient();
 | 
			
		||||
	const lastThemeRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
	// Fetch user preferences from backend
 | 
			
		||||
	const { data: userPreferences } = useQuery({
 | 
			
		||||
	// Use reactive authentication state from AuthContext
 | 
			
		||||
	// This ensures the query re-enables when user logs in
 | 
			
		||||
	const { user } = useAuth();
 | 
			
		||||
	const isAuthenticated = !!user;
 | 
			
		||||
 | 
			
		||||
	// Source of truth: Database (via userPreferences query)
 | 
			
		||||
	// localStorage is only used as a temporary cache until DB loads
 | 
			
		||||
	// Only fetch if user is authenticated to avoid 401 errors on login page
 | 
			
		||||
	const { data: userPreferences, isLoading: preferencesLoading } = useQuery({
 | 
			
		||||
		queryKey: ["userPreferences"],
 | 
			
		||||
		queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
			
		||||
		retry: 1,
 | 
			
		||||
		enabled: isAuthenticated, // Only run query if user is authenticated
 | 
			
		||||
		retry: 2,
 | 
			
		||||
		staleTime: 5 * 60 * 1000, // 5 minutes
 | 
			
		||||
		refetchOnWindowFocus: true, // Refetch when user returns to tab
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Update theme when preferences are loaded
 | 
			
		||||
	// Get theme from database (source of truth), fallback to user object from login, then localStorage cache, then default
 | 
			
		||||
	// Memoize to prevent recalculation on every render
 | 
			
		||||
	const colorThemeValue = useMemo(() => {
 | 
			
		||||
		return (
 | 
			
		||||
			userPreferences?.color_theme ||
 | 
			
		||||
			user?.color_theme ||
 | 
			
		||||
			localStorage.getItem("colorTheme") ||
 | 
			
		||||
			"cyber_blue"
 | 
			
		||||
		);
 | 
			
		||||
	}, [userPreferences?.color_theme, user?.color_theme]);
 | 
			
		||||
 | 
			
		||||
	// Only update state if the theme value actually changed (prevent loops)
 | 
			
		||||
	const [colorTheme, setColorTheme] = useState(() => colorThemeValue);
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		// Only update if the value actually changed from what we last saw (prevent loops)
 | 
			
		||||
		if (colorThemeValue !== lastThemeRef.current) {
 | 
			
		||||
			setColorTheme(colorThemeValue);
 | 
			
		||||
			lastThemeRef.current = colorThemeValue;
 | 
			
		||||
		}
 | 
			
		||||
	}, [colorThemeValue]);
 | 
			
		||||
 | 
			
		||||
	const isLoading = preferencesLoading;
 | 
			
		||||
 | 
			
		||||
	// Sync localStorage cache when DB data is available (for offline/performance)
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		if (userPreferences?.color_theme) {
 | 
			
		||||
			setColorTheme(userPreferences.color_theme);
 | 
			
		||||
			localStorage.setItem("colorTheme", userPreferences.color_theme);
 | 
			
		||||
		}
 | 
			
		||||
		setIsLoading(false);
 | 
			
		||||
	}, [userPreferences]);
 | 
			
		||||
	}, [userPreferences?.color_theme]);
 | 
			
		||||
 | 
			
		||||
	const updateColorTheme = async (theme) => {
 | 
			
		||||
	const updateColorTheme = useCallback(
 | 
			
		||||
		async (theme) => {
 | 
			
		||||
			// Store previous theme for potential revert
 | 
			
		||||
			const previousTheme = colorTheme;
 | 
			
		||||
 | 
			
		||||
			// Immediately update state for instant UI feedback
 | 
			
		||||
			setColorTheme(theme);
 | 
			
		||||
			lastThemeRef.current = theme;
 | 
			
		||||
 | 
			
		||||
			// Also update localStorage cache
 | 
			
		||||
			localStorage.setItem("colorTheme", theme);
 | 
			
		||||
 | 
			
		||||
		// Save to backend
 | 
			
		||||
			// Save to backend (source of truth)
 | 
			
		||||
			try {
 | 
			
		||||
				await userPreferencesAPI.update({ color_theme: theme });
 | 
			
		||||
 | 
			
		||||
				// Invalidate and refetch user preferences to ensure sync across tabs/browsers
 | 
			
		||||
				await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error("Failed to save color theme preference:", error);
 | 
			
		||||
			// Theme is already set locally, so user still sees the change
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
				// Revert to previous theme if save failed
 | 
			
		||||
				setColorTheme(previousTheme);
 | 
			
		||||
				lastThemeRef.current = previousTheme;
 | 
			
		||||
				localStorage.setItem("colorTheme", previousTheme);
 | 
			
		||||
 | 
			
		||||
	const value = {
 | 
			
		||||
				// Invalidate to refresh from DB
 | 
			
		||||
				await queryClient.invalidateQueries({ queryKey: ["userPreferences"] });
 | 
			
		||||
 | 
			
		||||
				// Show error to user if possible (could add toast notification here)
 | 
			
		||||
				throw error; // Re-throw so calling code can handle it
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		[colorTheme, queryClient],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	// Memoize themeConfig to prevent unnecessary re-renders
 | 
			
		||||
	const themeConfig = useMemo(
 | 
			
		||||
		() => THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
 | 
			
		||||
		[colorTheme],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	// Memoize the context value to prevent unnecessary re-renders
 | 
			
		||||
	const value = useMemo(
 | 
			
		||||
		() => ({
 | 
			
		||||
			colorTheme,
 | 
			
		||||
			setColorTheme: updateColorTheme,
 | 
			
		||||
		themeConfig: THEME_PRESETS[colorTheme] || THEME_PRESETS.default,
 | 
			
		||||
			themeConfig,
 | 
			
		||||
			isLoading,
 | 
			
		||||
	};
 | 
			
		||||
		}),
 | 
			
		||||
		[colorTheme, themeConfig, isLoading, updateColorTheme],
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<ColorThemeContext.Provider value={value}>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { useQuery } from "@tanstack/react-query";
 | 
			
		||||
import { createContext, useContext, useEffect, useState } from "react";
 | 
			
		||||
import { userPreferencesAPI } from "../utils/api";
 | 
			
		||||
import { useAuth } from "./AuthContext";
 | 
			
		||||
 | 
			
		||||
const ThemeContext = createContext();
 | 
			
		||||
 | 
			
		||||
@@ -26,21 +27,29 @@ export const ThemeProvider = ({ children }) => {
 | 
			
		||||
		return "light";
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Fetch user preferences from backend
 | 
			
		||||
	// Use reactive authentication state from AuthContext
 | 
			
		||||
	// This ensures the query re-enables when user logs in
 | 
			
		||||
	const { user } = useAuth();
 | 
			
		||||
	const isAuthenticated = !!user;
 | 
			
		||||
 | 
			
		||||
	// Fetch user preferences from backend (only if authenticated)
 | 
			
		||||
	const { data: userPreferences } = useQuery({
 | 
			
		||||
		queryKey: ["userPreferences"],
 | 
			
		||||
		queryFn: () => userPreferencesAPI.get().then((res) => res.data),
 | 
			
		||||
		enabled: isAuthenticated, // Only run query if user is authenticated
 | 
			
		||||
		retry: 1,
 | 
			
		||||
		staleTime: 5 * 60 * 1000, // 5 minutes
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Sync with user preferences from backend
 | 
			
		||||
	// Sync with user preferences from backend or user object from login
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		if (userPreferences?.theme_preference) {
 | 
			
		||||
			setTheme(userPreferences.theme_preference);
 | 
			
		||||
			localStorage.setItem("theme", userPreferences.theme_preference);
 | 
			
		||||
		const preferredTheme =
 | 
			
		||||
			userPreferences?.theme_preference || user?.theme_preference;
 | 
			
		||||
		if (preferredTheme) {
 | 
			
		||||
			setTheme(preferredTheme);
 | 
			
		||||
			localStorage.setItem("theme", preferredTheme);
 | 
			
		||||
		}
 | 
			
		||||
	}, [userPreferences]);
 | 
			
		||||
	}, [userPreferences, user?.theme_preference]);
 | 
			
		||||
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		// Apply theme to document
 | 
			
		||||
 
 | 
			
		||||
@@ -248,7 +248,8 @@ const Login = () => {
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error("Failed to fetch GitHub data:", error);
 | 
			
		||||
				// Set fallback data if nothing cached
 | 
			
		||||
				if (!latestRelease) {
 | 
			
		||||
				const cachedRelease = localStorage.getItem("githubLatestRelease");
 | 
			
		||||
				if (!cachedRelease) {
 | 
			
		||||
					setLatestRelease({
 | 
			
		||||
						version: "v1.3.0",
 | 
			
		||||
						name: "Latest Release",
 | 
			
		||||
@@ -260,7 +261,7 @@ const Login = () => {
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		fetchGitHubData();
 | 
			
		||||
	}, [latestRelease]);
 | 
			
		||||
	}, []); // Run once on mount
 | 
			
		||||
 | 
			
		||||
	const handleSubmit = async (e) => {
 | 
			
		||||
		e.preventDefault();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										285
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										285
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "patchmon",
 | 
			
		||||
	"version": "1.3.2",
 | 
			
		||||
	"version": "1.3.1",
 | 
			
		||||
	"lockfileVersion": 3,
 | 
			
		||||
	"requires": true,
 | 
			
		||||
	"packages": {
 | 
			
		||||
		"": {
 | 
			
		||||
			"name": "patchmon",
 | 
			
		||||
			"version": "1.3.2",
 | 
			
		||||
			"version": "1.3.1",
 | 
			
		||||
			"license": "AGPL-3.0",
 | 
			
		||||
			"workspaces": [
 | 
			
		||||
				"backend",
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
		},
 | 
			
		||||
		"backend": {
 | 
			
		||||
			"name": "patchmon-backend",
 | 
			
		||||
			"version": "1.3.2",
 | 
			
		||||
			"version": "1.3.1",
 | 
			
		||||
			"license": "AGPL-3.0",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@bull-board/api": "^6.13.1",
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
				"cors": "^2.8.5",
 | 
			
		||||
				"dotenv": "^16.4.7",
 | 
			
		||||
				"express": "^4.21.2",
 | 
			
		||||
				"express-rate-limit": "^8.0.0",
 | 
			
		||||
				"express-rate-limit": "^7.5.0",
 | 
			
		||||
				"express-validator": "^7.2.0",
 | 
			
		||||
				"helmet": "^8.0.0",
 | 
			
		||||
				"ioredis": "^5.8.1",
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
		},
 | 
			
		||||
		"frontend": {
 | 
			
		||||
			"name": "patchmon-frontend",
 | 
			
		||||
			"version": "1.3.2",
 | 
			
		||||
			"version": "1.3.1",
 | 
			
		||||
			"license": "AGPL-3.0",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@dnd-kit/core": "^6.3.1",
 | 
			
		||||
@@ -134,7 +134,6 @@
 | 
			
		||||
			"version": "7.28.4",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@babel/code-frame": "^7.27.1",
 | 
			
		||||
				"@babel/generator": "^7.28.3",
 | 
			
		||||
@@ -387,74 +386,6 @@
 | 
			
		||||
				"@biomejs/cli-win32-x64": "2.3.0"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-darwin-arm64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-3cJVT0Z5pbTkoBmbjmDZTDFYxIkRcrs9sYVJbIBHU8E6qQxgXAaBfSVjjCreG56rfDuQBr43GzwzmaHPcu4vlw==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"darwin"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-darwin-x64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-6LIkhglh3UGjuDqJXsK42qCA0XkD1Ke4K/raFOii7QQPbM8Pia7Qj2Hji4XuF2/R78hRmEx7uKJH3t/Y9UahtQ==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"darwin"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-linux-arm64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-uhAsbXySX7xsXahegDg5h3CDgfMcRsJvWLFPG0pjkylgBb9lErbK2C0UINW52zhwg0cPISB09lxHPxCau4e2xA==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"linux"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-linux-arm64-musl": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-nDksoFdwZ2YrE7NiYDhtMhL2UgFn8Kb7Y0bYvnTAakHnqEdb4lKindtBc1f+xg2Snz0JQhJUYO7r9CDBosRU5w==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"linux"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-linux-x64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
@@ -470,57 +401,6 @@
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-linux-x64-musl": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-+i9UcJwl99uAhtRQDz9jUAh+Xkb097eekxs/D9j4deWDg5/yB/jPWzISe1nBHvlzTXsdUSj0VvB4Go2DSpKIMw==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"linux"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-win32-arm64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-ynjmsJLIKrAjC3CCnKMMhzcnNy8dbQWjKfSU5YA0mIruTxBNMbkAJp+Pr2iV7/hFou+66ZSD/WV8hmLEmhUaXA==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"win32"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@biomejs/cli-win32-x64": {
 | 
			
		||||
			"version": "2.3.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.0.tgz",
 | 
			
		||||
			"integrity": "sha512-zOCYmCRVkWXc9v8P7OLbLlGGMxQTKMvi+5IC4v7O8DkjLCOHRzRVK/Lno2pGZNo0lzKM60pcQOhH8HVkXMQdFg==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT OR Apache-2.0",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"win32"
 | 
			
		||||
			],
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=14.21.3"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@bull-board/api": {
 | 
			
		||||
			"version": "6.13.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
@@ -544,7 +424,6 @@
 | 
			
		||||
		"node_modules/@bull-board/ui": {
 | 
			
		||||
			"version": "6.13.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@bull-board/api": "6.13.1"
 | 
			
		||||
			}
 | 
			
		||||
@@ -578,7 +457,6 @@
 | 
			
		||||
		"node_modules/@dnd-kit/core": {
 | 
			
		||||
			"version": "6.3.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@dnd-kit/accessibility": "^3.1.1",
 | 
			
		||||
				"@dnd-kit/utilities": "^3.2.2",
 | 
			
		||||
@@ -1019,7 +897,6 @@
 | 
			
		||||
			"version": "18.3.24",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@types/prop-types": "*",
 | 
			
		||||
				"csstype": "^3.0.2"
 | 
			
		||||
@@ -1267,7 +1144,6 @@
 | 
			
		||||
				}
 | 
			
		||||
			],
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"baseline-browser-mapping": "^2.8.3",
 | 
			
		||||
				"caniuse-lite": "^1.0.30001741",
 | 
			
		||||
@@ -1457,7 +1333,6 @@
 | 
			
		||||
		"node_modules/chart.js": {
 | 
			
		||||
			"version": "4.5.0",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@kurkle/color": "^0.3.0"
 | 
			
		||||
			},
 | 
			
		||||
@@ -2032,7 +1907,6 @@
 | 
			
		||||
		"node_modules/express": {
 | 
			
		||||
			"version": "4.21.2",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"accepts": "~1.3.8",
 | 
			
		||||
				"array-flatten": "1.1.1",
 | 
			
		||||
@@ -2075,13 +1949,8 @@
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/express-rate-limit": {
 | 
			
		||||
			"version": "8.2.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
 | 
			
		||||
			"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
 | 
			
		||||
			"version": "7.5.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"ip-address": "10.0.1"
 | 
			
		||||
			},
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">= 16"
 | 
			
		||||
			},
 | 
			
		||||
@@ -2579,15 +2448,6 @@
 | 
			
		||||
				"url": "https://opencollective.com/ioredis"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/ip-address": {
 | 
			
		||||
			"version": "10.0.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
 | 
			
		||||
			"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">= 12"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/ipaddr.js": {
 | 
			
		||||
			"version": "1.9.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
@@ -2812,76 +2672,6 @@
 | 
			
		||||
				"lefthook-windows-x64": "1.13.5"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-darwin-arm64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-BYt5CnAOXasVCS6i+A4ljUo9xru/B5uMFD6EWHhs3R26jGF7mBSDxM3ErzXTUaJRTP0kQI/XBmgqBryBqoqZOQ==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"darwin"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-darwin-x64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-ZDtLBzvI5e26C/RZ4irOHpELTd22x9lDTgF2+eCYcnrBWOkB7800V8tuAvBybsLGvg6JwKjFxn+NTRNZnCC2hw==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"darwin"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-freebsd-arm64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-uQ/kQZSSedw74aGCpsfOPN4yVt3klg8grOP6gHQOCRUMv5oK/Lj3pe1PylpTuuhxWORWRzkauPMot26J0OZZdA==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"freebsd"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-freebsd-x64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-6czek8XagVrI7ExURawkfrfX40Qjc/wktc8bLq/iXfRlmdvKDMrx2FrA82mDfEVCAEz+tTvkteK1TfR3icYF3Q==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"freebsd"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-linux-arm64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-MjWtiuW1br+rpTtgG1KGV53mSGtL5MWQwgafYzrFleJ89fKb86F4TD/4mVNzk5thmZ+HVPZw9bRZGUHFBnNJWg==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"linux"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-linux-x64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
@@ -2894,62 +2684,6 @@
 | 
			
		||||
				"linux"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-openbsd-arm64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-lYXrWf0/hBrwtG8ceaHq886bcqRKh3Lfv+jZJs+ykMLB6L/kaqk8tA4V2NHWydQ5h56o45ugs/580nMz36ZdRg==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"openbsd"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-openbsd-x64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-Ba1JrsRbfan4WKd8Q7gUhTxCUuppXzirDObd3JxpLRSLxA47yxhjMv7KByDunRDTvzTgsXoykZI6mPupkc1JiQ==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"openbsd"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-windows-arm64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-Y/CpmEIb0hlFe+kTT/efWgX6+/gUTp5NItTF+gmUrY1/G/bTLIxdIRS7WpodVM0MEN24sOrQVTSi9DN9FvGoGg==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"arm64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"win32"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lefthook-windows-x64": {
 | 
			
		||||
			"version": "1.13.5",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-1.13.5.tgz",
 | 
			
		||||
			"integrity": "sha512-WJBqGNBlFJnunRwy12QyaDHdGULtostPqpYSZSS4boFJDY0lP5qtz9lAGmJ49aA5GQ19jrnDjGLwVPFiwIqksQ==",
 | 
			
		||||
			"cpu": [
 | 
			
		||||
				"x64"
 | 
			
		||||
			],
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"optional": true,
 | 
			
		||||
			"os": [
 | 
			
		||||
				"win32"
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/lilconfig": {
 | 
			
		||||
			"version": "3.1.3",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
@@ -3562,7 +3296,6 @@
 | 
			
		||||
				}
 | 
			
		||||
			],
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"nanoid": "^3.3.11",
 | 
			
		||||
				"picocolors": "^1.1.1",
 | 
			
		||||
@@ -3692,7 +3425,6 @@
 | 
			
		||||
			"devOptional": true,
 | 
			
		||||
			"hasInstallScript": true,
 | 
			
		||||
			"license": "Apache-2.0",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@prisma/config": "6.16.2",
 | 
			
		||||
				"@prisma/engines": "6.16.2"
 | 
			
		||||
@@ -3882,7 +3614,6 @@
 | 
			
		||||
		"node_modules/react": {
 | 
			
		||||
			"version": "18.3.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"loose-envify": "^1.1.0"
 | 
			
		||||
			},
 | 
			
		||||
@@ -3901,7 +3632,6 @@
 | 
			
		||||
		"node_modules/react-dom": {
 | 
			
		||||
			"version": "18.3.1",
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"loose-envify": "^1.1.0",
 | 
			
		||||
				"scheduler": "^0.23.2"
 | 
			
		||||
@@ -4619,7 +4349,6 @@
 | 
			
		||||
			"version": "4.0.3",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=12"
 | 
			
		||||
			},
 | 
			
		||||
@@ -4772,7 +4501,6 @@
 | 
			
		||||
			"version": "7.1.7",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"esbuild": "^0.25.0",
 | 
			
		||||
				"fdir": "^6.5.0",
 | 
			
		||||
@@ -4862,7 +4590,6 @@
 | 
			
		||||
			"version": "4.0.3",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"peer": true,
 | 
			
		||||
			"engines": {
 | 
			
		||||
				"node": ">=12"
 | 
			
		||||
			},
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								setup.sh
									
									
									
									
									
								
							@@ -66,27 +66,27 @@ SELECTED_SERVICE_NAME=""
 | 
			
		||||
 | 
			
		||||
# Functions
 | 
			
		||||
print_status() {
 | 
			
		||||
    echo -e "${GREEN}✅ $1${NC}"
 | 
			
		||||
    printf "${GREEN}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print_info() {
 | 
			
		||||
    echo -e "${BLUE}ℹ️  $1${NC}"
 | 
			
		||||
    printf "${BLUE}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print_error() {
 | 
			
		||||
    echo -e "${RED}❌ $1${NC}"
 | 
			
		||||
    printf "${RED}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print_warning() {
 | 
			
		||||
    echo -e "${YELLOW}⚠️  $1${NC}"
 | 
			
		||||
    printf "${YELLOW}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print_question() {
 | 
			
		||||
    echo -e "${BLUE}❓ $1${NC}"
 | 
			
		||||
    printf "${BLUE}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
print_success() {
 | 
			
		||||
    echo -e "${GREEN}🎉 $1${NC}"
 | 
			
		||||
    printf "${GREEN}%s${NC}\n" "$1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Interactive input functions
 | 
			
		||||
@@ -1657,7 +1657,7 @@ start_services() {
 | 
			
		||||
        local logs=$(journalctl -u "$SERVICE_NAME" -n 50 --no-pager 2>/dev/null || echo "")
 | 
			
		||||
        
 | 
			
		||||
        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
			
		||||
            print_error "❌ Detected Redis authentication error!"
 | 
			
		||||
            print_error "Detected Redis authentication error!"
 | 
			
		||||
            print_info "The service cannot authenticate with Redis."
 | 
			
		||||
            echo ""
 | 
			
		||||
            print_info "Current Redis configuration in .env:"
 | 
			
		||||
@@ -1681,18 +1681,18 @@ start_services() {
 | 
			
		||||
            print_info "     cat /etc/redis/users.acl"
 | 
			
		||||
            echo ""
 | 
			
		||||
        elif echo "$logs" | grep -q "ECONNREFUSED.*postgresql\|Connection refused.*5432"; then
 | 
			
		||||
            print_error "❌ Detected PostgreSQL connection error!"
 | 
			
		||||
            print_error "Detected PostgreSQL connection error!"
 | 
			
		||||
            print_info "Check if PostgreSQL is running:"
 | 
			
		||||
            print_info "  systemctl status postgresql"
 | 
			
		||||
        elif echo "$logs" | grep -q "ECONNREFUSED.*redis\|Connection refused.*6379"; then
 | 
			
		||||
            print_error "❌ Detected Redis connection error!"
 | 
			
		||||
            print_error "Detected Redis connection error!"
 | 
			
		||||
            print_info "Check if Redis is running:"
 | 
			
		||||
            print_info "  systemctl status redis-server"
 | 
			
		||||
        elif echo "$logs" | grep -q "database.*does not exist"; then
 | 
			
		||||
            print_error "❌ Database does not exist!"
 | 
			
		||||
            print_error "Database does not exist!"
 | 
			
		||||
            print_info "Database: $DB_NAME"
 | 
			
		||||
        elif echo "$logs" | grep -q "Error:"; then
 | 
			
		||||
            print_error "❌ Application error detected in logs"
 | 
			
		||||
            print_error "Application error detected in logs"
 | 
			
		||||
        fi
 | 
			
		||||
        
 | 
			
		||||
        echo ""
 | 
			
		||||
@@ -1741,9 +1741,9 @@ async function updateSettings() {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    console.log('✅ Database settings updated successfully');
 | 
			
		||||
    console.log('Database settings updated successfully');
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('❌ Error updating settings:', error.message);
 | 
			
		||||
    console.error('Error updating settings:', error.message);
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  } finally {
 | 
			
		||||
    await prisma.\$disconnect();
 | 
			
		||||
@@ -1867,7 +1867,7 @@ EOF
 | 
			
		||||
    if [ -f "$SUMMARY_FILE" ]; then
 | 
			
		||||
        print_status "Deployment summary appended to: $SUMMARY_FILE"
 | 
			
		||||
    else
 | 
			
		||||
        print_error "⚠️  Failed to append to deployment-info.txt file"
 | 
			
		||||
        print_error "Failed to append to deployment-info.txt file"
 | 
			
		||||
        return 1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
@@ -1949,7 +1949,7 @@ EOF
 | 
			
		||||
        print_status "Deployment information saved to: $INFO_FILE"
 | 
			
		||||
        print_info "File details: $(ls -lh "$INFO_FILE" | awk '{print $5, $9}')"
 | 
			
		||||
    else
 | 
			
		||||
        print_error "⚠️  Failed to create deployment-info.txt file"
 | 
			
		||||
        print_error "Failed to create deployment-info.txt file"
 | 
			
		||||
        return 1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
@@ -2142,7 +2142,7 @@ deploy_instance() {
 | 
			
		||||
    log_message "Backend port: $BACKEND_PORT"
 | 
			
		||||
    log_message "SSL enabled: $USE_LETSENCRYPT"
 | 
			
		||||
    
 | 
			
		||||
    print_status "🎉 PatchMon instance deployed successfully!"
 | 
			
		||||
    print_status "PatchMon instance deployed successfully!"
 | 
			
		||||
    echo ""
 | 
			
		||||
    print_info "Next steps:"
 | 
			
		||||
    echo "  • Visit your URL: $SERVER_PROTOCOL_SEL://$FQDN (ensure DNS is configured)"
 | 
			
		||||
@@ -3236,7 +3236,7 @@ update_installation() {
 | 
			
		||||
    sleep 5
 | 
			
		||||
    
 | 
			
		||||
    if systemctl is-active --quiet "$service_name"; then
 | 
			
		||||
        print_success "✅ Update completed successfully!"
 | 
			
		||||
        print_success "Update completed successfully!"
 | 
			
		||||
        print_status "Service $service_name is running"
 | 
			
		||||
        
 | 
			
		||||
        # Get new version
 | 
			
		||||
@@ -3264,7 +3264,7 @@ update_installation() {
 | 
			
		||||
        local logs=$(journalctl -u "$service_name" -n 50 --no-pager 2>/dev/null || echo "")
 | 
			
		||||
        
 | 
			
		||||
        if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then
 | 
			
		||||
            print_error "❌ Detected Redis authentication error!"
 | 
			
		||||
            print_error "Detected Redis authentication error!"
 | 
			
		||||
            print_info "The service cannot authenticate with Redis."
 | 
			
		||||
            echo ""
 | 
			
		||||
            print_info "Current Redis configuration in .env:"
 | 
			
		||||
@@ -3281,12 +3281,12 @@ update_installation() {
 | 
			
		||||
            print_info "     redis-cli --user $test_user --pass $test_pass -n ${test_db:-0} ping"
 | 
			
		||||
            echo ""
 | 
			
		||||
        elif echo "$logs" | grep -q "ECONNREFUSED"; then
 | 
			
		||||
            print_error "❌ Detected connection refused error!"
 | 
			
		||||
            print_error "Detected connection refused error!"
 | 
			
		||||
            print_info "Check if required services are running:"
 | 
			
		||||
            print_info "  systemctl status postgresql"
 | 
			
		||||
            print_info "  systemctl status redis-server"
 | 
			
		||||
        elif echo "$logs" | grep -q "Error:"; then
 | 
			
		||||
            print_error "❌ Application error detected in logs"
 | 
			
		||||
            print_error "Application error detected in logs"
 | 
			
		||||
        fi
 | 
			
		||||
        
 | 
			
		||||
        echo ""
 | 
			
		||||
@@ -3319,7 +3319,7 @@ main() {
 | 
			
		||||
    # Handle update mode
 | 
			
		||||
    if [ "$UPDATE_MODE" = "true" ]; then
 | 
			
		||||
        print_banner
 | 
			
		||||
        print_info "🔄 PatchMon Update Mode"
 | 
			
		||||
        print_info "PatchMon Update Mode"
 | 
			
		||||
        echo ""
 | 
			
		||||
        
 | 
			
		||||
        # Select installation to update
 | 
			
		||||
@@ -3335,7 +3335,7 @@ main() {
 | 
			
		||||
    # Check if existing installations are present
 | 
			
		||||
    local existing_installs=($(detect_installations))
 | 
			
		||||
    if [ ${#existing_installs[@]} -gt 0 ]; then
 | 
			
		||||
        print_warning "⚠️  Found ${#existing_installs[@]} existing PatchMon installation(s):"
 | 
			
		||||
        print_warning "Found ${#existing_installs[@]} existing PatchMon installation(s):"
 | 
			
		||||
        for install in "${existing_installs[@]}"; do
 | 
			
		||||
            print_info "   - $install"
 | 
			
		||||
        done
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user