mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-31 20:13:50 +00:00 
			
		
		
		
	Updated frontend to snake_case and fixed bugs with some pages that were not showing. Fixed authentication side.
This commit is contained in:
		| @@ -5,39 +5,39 @@ const prisma = new PrismaClient(); | ||||
| async function checkAgentVersion() { | ||||
|   try { | ||||
|     // Check current agent version in database | ||||
|     const agentVersion = await prisma.agentVersion.findFirst({ | ||||
|     const agentVersion = await prisma.agent_versions.findFirst({ | ||||
|       where: { version: '1.2.6' } | ||||
|     }); | ||||
|      | ||||
|     if (agentVersion) { | ||||
|       console.log('✅ Agent version 1.2.6 found in database'); | ||||
|       console.log('Version:', agentVersion.version); | ||||
|       console.log('Is Default:', agentVersion.isDefault); | ||||
|       console.log('Script Content Length:', agentVersion.scriptContent?.length || 0); | ||||
|       console.log('Created At:', agentVersion.createdAt); | ||||
|       console.log('Updated At:', agentVersion.updatedAt); | ||||
|       console.log('Is Default:', agentVersion.is_default); | ||||
|       console.log('Script Content Length:', agentVersion.script_content?.length || 0); | ||||
|       console.log('Created At:', agentVersion.created_at); | ||||
|       console.log('Updated At:', agentVersion.updated_at); | ||||
|        | ||||
|       // Check if script content contains the current version | ||||
|       if (agentVersion.scriptContent && agentVersion.scriptContent.includes('AGENT_VERSION="1.2.6"')) { | ||||
|       if (agentVersion.script_content && agentVersion.script_content.includes('AGENT_VERSION="1.2.6"')) { | ||||
|         console.log('✅ Script content contains correct version 1.2.6'); | ||||
|       } else { | ||||
|         console.log('❌ Script content does not contain version 1.2.6'); | ||||
|       } | ||||
|        | ||||
|       // Check if script content contains system info functions | ||||
|       if (agentVersion.scriptContent && agentVersion.scriptContent.includes('get_hardware_info()')) { | ||||
|       if (agentVersion.script_content && agentVersion.script_content.includes('get_hardware_info()')) { | ||||
|         console.log('✅ Script content contains hardware info function'); | ||||
|       } else { | ||||
|         console.log('❌ Script content missing hardware info function'); | ||||
|       } | ||||
|        | ||||
|       if (agentVersion.scriptContent && agentVersion.scriptContent.includes('get_network_info()')) { | ||||
|       if (agentVersion.script_content && agentVersion.script_content.includes('get_network_info()')) { | ||||
|         console.log('✅ Script content contains network info function'); | ||||
|       } else { | ||||
|         console.log('❌ Script content missing network info function'); | ||||
|       } | ||||
|        | ||||
|       if (agentVersion.scriptContent && agentVersion.scriptContent.includes('get_system_info()')) { | ||||
|       if (agentVersion.script_content && agentVersion.script_content.includes('get_system_info()')) { | ||||
|         console.log('✅ Script content contains system info function'); | ||||
|       } else { | ||||
|         console.log('❌ Script content missing system info function'); | ||||
| @@ -49,12 +49,12 @@ async function checkAgentVersion() { | ||||
|      | ||||
|     // List all agent versions | ||||
|     console.log('\n=== All Agent Versions ==='); | ||||
|     const allVersions = await prisma.agentVersion.findMany({ | ||||
|       orderBy: { createdAt: 'desc' } | ||||
|     const allVersions = await prisma.agent_versions.findMany({ | ||||
|       orderBy: { created_at: 'desc' } | ||||
|     }); | ||||
|      | ||||
|     allVersions.forEach(version => { | ||||
|       console.log(`Version: ${version.version}, Default: ${version.isDefault}, Length: ${version.scriptContent?.length || 0}`); | ||||
|       console.log(`Version: ${version.version}, Default: ${version.is_default}, Length: ${version.script_content?.length || 0}`); | ||||
|     }); | ||||
|      | ||||
|   } catch (error) { | ||||
|   | ||||
| @@ -25,7 +25,9 @@ const authenticateToken = async (req, res, next) => { | ||||
|         email: true, | ||||
|         role: true, | ||||
|         is_active: true, | ||||
|         last_login: true | ||||
|         last_login: true, | ||||
|         created_at: true, | ||||
|         updated_at: true | ||||
|       } | ||||
|     }); | ||||
|  | ||||
| @@ -79,7 +81,10 @@ const optionalAuth = async (req, res, next) => { | ||||
|           username: true, | ||||
|           email: true, | ||||
|           role: true, | ||||
|           is_active: true | ||||
|           is_active: true, | ||||
|           last_login: true, | ||||
|           created_at: true, | ||||
|           updated_at: true | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ const requirePermission = (permission) => { | ||||
|       if (!rolePermissions[permission]) { | ||||
|         return res.status(403).json({  | ||||
|           error: 'Insufficient permissions', | ||||
|           message: `You don't have permission to ${permission.replace('can', '').toLowerCase()}` | ||||
|           message: `You don't have permission to ${permission.replace('can_', '').replace('_', ' ')}` | ||||
|         }); | ||||
|       } | ||||
|  | ||||
| @@ -32,17 +32,17 @@ const requirePermission = (permission) => { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| // Specific permission middlewares | ||||
| const requireViewDashboard = requirePermission('canViewDashboard'); | ||||
| const requireViewHosts = requirePermission('canViewHosts'); | ||||
| const requireManageHosts = requirePermission('canManageHosts'); | ||||
| const requireViewPackages = requirePermission('canViewPackages'); | ||||
| const requireManagePackages = requirePermission('canManagePackages'); | ||||
| const requireViewUsers = requirePermission('canViewUsers'); | ||||
| const requireManageUsers = requirePermission('canManageUsers'); | ||||
| const requireViewReports = requirePermission('canViewReports'); | ||||
| const requireExportData = requirePermission('canExportData'); | ||||
| const requireManageSettings = requirePermission('canManageSettings'); | ||||
| // Specific permission middlewares - using snake_case field names | ||||
| const requireViewDashboard = requirePermission('can_view_dashboard'); | ||||
| const requireViewHosts = requirePermission('can_view_hosts'); | ||||
| const requireManageHosts = requirePermission('can_manage_hosts'); | ||||
| const requireViewPackages = requirePermission('can_view_packages'); | ||||
| const requireManagePackages = requirePermission('can_manage_packages'); | ||||
| const requireViewUsers = requirePermission('can_view_users'); | ||||
| const requireManageUsers = requirePermission('can_manage_users'); | ||||
| const requireViewReports = requirePermission('can_view_reports'); | ||||
| const requireExportData = requirePermission('can_export_data'); | ||||
| const requireManageSettings = requirePermission('can_manage_settings'); | ||||
|  | ||||
| module.exports = { | ||||
|   requirePermission, | ||||
|   | ||||
| @@ -426,6 +426,10 @@ router.post('/login', [ | ||||
|         email: true, | ||||
|         password_hash: true, | ||||
|         role: true, | ||||
|         is_active: true, | ||||
|         last_login: true, | ||||
|         created_at: true, | ||||
|         updated_at: true, | ||||
|         tfa_enabled: true | ||||
|       } | ||||
|     }); | ||||
| @@ -468,7 +472,11 @@ router.post('/login', [ | ||||
|         id: user.id, | ||||
|         username: user.username, | ||||
|         email: user.email, | ||||
|         role: user.role | ||||
|         role: user.role, | ||||
|         is_active: user.is_active, | ||||
|         last_login: user.last_login, | ||||
|         created_at: user.created_at, | ||||
|         updated_at: user.updated_at | ||||
|       } | ||||
|     }); | ||||
|   } catch (error) { | ||||
|   | ||||
| @@ -134,7 +134,7 @@ const validateApiCredentials = async (req, res, next) => { | ||||
|  | ||||
| // Admin endpoint to create a new host manually (replaces auto-registration) | ||||
| router.post('/create', authenticateToken, requireManageHosts, [ | ||||
|   body('friendlyName').isLength({ min: 1 }).withMessage('Friendly name is required'), | ||||
|   body('friendly_name').isLength({ min: 1 }).withMessage('Friendly name is required'), | ||||
|   body('hostGroupId').optional() | ||||
| ], async (req, res) => { | ||||
|   try { | ||||
| @@ -143,14 +143,14 @@ router.post('/create', authenticateToken, requireManageHosts, [ | ||||
|       return res.status(400).json({ errors: errors.array() }); | ||||
|     } | ||||
|  | ||||
|     const { friendlyName, hostGroupId } = req.body; | ||||
|     const { friendly_name, hostGroupId } = req.body; | ||||
|      | ||||
|     // Generate unique API credentials for this host | ||||
|     const { apiId, apiKey } = generateApiCredentials(); | ||||
|      | ||||
|     // Check if host already exists | ||||
|     const existingHost = await prisma.hosts.findUnique({ | ||||
|       where: { friendly_name: friendlyName } | ||||
|       where: { friendly_name: friendly_name } | ||||
|     }); | ||||
|  | ||||
|     if (existingHost) { | ||||
| @@ -172,7 +172,7 @@ router.post('/create', authenticateToken, requireManageHosts, [ | ||||
|     const host = await prisma.hosts.create({ | ||||
|       data: { | ||||
|         id: uuidv4(), | ||||
|         friendly_name: friendlyName, | ||||
|         friendly_name: friendly_name, | ||||
|         os_type: 'unknown', // Will be updated when agent connects | ||||
|         os_version: 'unknown', // Will be updated when agent connects | ||||
|         ip: null, // Will be updated when agent connects | ||||
| @@ -786,7 +786,7 @@ router.delete('/:hostId', authenticateToken, requireManageHosts, async (req, res | ||||
|  | ||||
| // Toggle host auto-update setting | ||||
| router.patch('/:hostId/auto-update', authenticateToken, requireManageHosts, [ | ||||
|   body('autoUpdate').isBoolean().withMessage('Auto-update must be a boolean') | ||||
|   body('auto_update').isBoolean().withMessage('Auto-update must be a boolean') | ||||
| ], async (req, res) => { | ||||
|   try { | ||||
|     const errors = validationResult(req); | ||||
| @@ -795,12 +795,12 @@ router.patch('/:hostId/auto-update', authenticateToken, requireManageHosts, [ | ||||
|     } | ||||
|  | ||||
|     const { hostId } = req.params; | ||||
|     const { autoUpdate } = req.body; | ||||
|     const { auto_update } = req.body; | ||||
|  | ||||
|     const host = await prisma.hosts.update({ | ||||
|       where: { id: hostId }, | ||||
|       data: {  | ||||
|         auto_update: autoUpdate, | ||||
|         auto_update: auto_update, | ||||
|         updated_at: new Date() | ||||
|       } | ||||
|     }); | ||||
| @@ -1011,7 +1011,7 @@ router.delete('/agent/versions/:versionId', authenticateToken, requireManageSett | ||||
|  | ||||
| // Update host friendly name (admin only) | ||||
| router.patch('/:hostId/friendly-name', authenticateToken, requireManageHosts, [ | ||||
|   body('friendlyName').isLength({ min: 1, max: 100 }).withMessage('Friendly name must be between 1 and 100 characters') | ||||
|   body('friendly_name').isLength({ min: 1, max: 100 }).withMessage('Friendly name must be between 1 and 100 characters') | ||||
| ], async (req, res) => { | ||||
|   try { | ||||
|     const errors = validationResult(req); | ||||
| @@ -1020,7 +1020,7 @@ router.patch('/:hostId/friendly-name', authenticateToken, requireManageHosts, [ | ||||
|     } | ||||
|  | ||||
|     const { hostId } = req.params; | ||||
|     const { friendlyName } = req.body; | ||||
|     const { friendly_name } = req.body; | ||||
|  | ||||
|     // Check if host exists | ||||
|     const host = await prisma.hosts.findUnique({ | ||||
| @@ -1034,7 +1034,7 @@ router.patch('/:hostId/friendly-name', authenticateToken, requireManageHosts, [ | ||||
|     // Check if friendly name is already taken by another host | ||||
|     const existingHost = await prisma.hosts.findFirst({ | ||||
|       where: { | ||||
|         friendly_name: friendlyName, | ||||
|         friendly_name: friendly_name, | ||||
|         id: { not: hostId } | ||||
|       } | ||||
|     }); | ||||
| @@ -1046,7 +1046,7 @@ router.patch('/:hostId/friendly-name', authenticateToken, requireManageHosts, [ | ||||
|     // Update the friendly name | ||||
|     const updatedHost = await prisma.hosts.update({ | ||||
|       where: { id: hostId }, | ||||
|       data: { friendly_name: friendlyName }, | ||||
|       data: { friendly_name: friendly_name }, | ||||
|       select: { | ||||
|         id: true, | ||||
|         friendly_name: true, | ||||
|   | ||||
| @@ -153,16 +153,16 @@ router.get('/user-permissions', authenticateToken, async (req, res) => { | ||||
|       // If no specific permissions found, return default admin permissions | ||||
|       return res.json({ | ||||
|         role: userRole, | ||||
|         canViewDashboard: true, | ||||
|         canViewHosts: true, | ||||
|         canManageHosts: true, | ||||
|         canViewPackages: true, | ||||
|         canManagePackages: true, | ||||
|         canViewUsers: true, | ||||
|         canManageUsers: true, | ||||
|         canViewReports: true, | ||||
|         canExportData: true, | ||||
|         canManageSettings: true, | ||||
|         can_view_dashboard: true, | ||||
|         can_view_hosts: true, | ||||
|         can_manage_hosts: true, | ||||
|         can_view_packages: true, | ||||
|         can_manage_packages: true, | ||||
|         can_view_users: true, | ||||
|         can_manage_users: true, | ||||
|         can_view_reports: true, | ||||
|         can_export_data: true, | ||||
|         can_manage_settings: true, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ router.get('/setup', authenticateToken, async (req, res) => { | ||||
|     // Check if user already has TFA enabled | ||||
|     const user = await prisma.users.findUnique({ | ||||
|       where: { id: userId }, | ||||
|       select: { tfaEnabled: true, tfaSecret: true } | ||||
|       select: { tfa_enabled: true, tfa_secret: true } | ||||
|     }); | ||||
|  | ||||
|     if (user.tfa_enabled) { | ||||
| @@ -86,7 +86,7 @@ router.post('/verify-setup', authenticateToken, [ | ||||
|  | ||||
|     // Verify the token | ||||
|     const verified = speakeasy.totp.verify({ | ||||
|       secret: user.tfaSecret, | ||||
|       secret: user.tfa_secret, | ||||
|       encoding: 'base32', | ||||
|       token: token, | ||||
|       window: 2 // Allow 2 time windows (60 seconds) for clock drift | ||||
| @@ -201,7 +201,7 @@ router.post('/regenerate-backup-codes', authenticateToken, async (req, res) => { | ||||
|     // Check if TFA is enabled | ||||
|     const user = await prisma.users.findUnique({ | ||||
|       where: { id: userId }, | ||||
|       select: { tfaEnabled: true } | ||||
|       select: { tfa_enabled: true } | ||||
|     }); | ||||
|  | ||||
|     if (!user.tfa_enabled) { | ||||
| @@ -219,7 +219,7 @@ router.post('/regenerate-backup-codes', authenticateToken, async (req, res) => { | ||||
|     await prisma.users.update({ | ||||
|       where: { id: userId }, | ||||
|       data: { | ||||
|         tfaBackupCodes: JSON.stringify(backupCodes) | ||||
|         tfa_backup_codes: JSON.stringify(backupCodes) | ||||
|       } | ||||
|     }); | ||||
|  | ||||
| @@ -265,7 +265,7 @@ router.post('/verify', [ | ||||
|     } | ||||
|  | ||||
|     // Check if it's a backup code | ||||
|     const backupCodes = user.tfaBackupCodes ? JSON.parse(user.tfaBackupCodes) : []; | ||||
|     const backupCodes = user.tfa_backup_codes ? JSON.parse(user.tfa_backup_codes) : []; | ||||
|     const isBackupCode = backupCodes.includes(token); | ||||
|  | ||||
|     let verified = false; | ||||
| @@ -276,7 +276,7 @@ router.post('/verify', [ | ||||
|       await prisma.users.update({ | ||||
|         where: { id: user.id }, | ||||
|         data: { | ||||
|           tfaBackupCodes: JSON.stringify(updatedBackupCodes) | ||||
|           tfa_backup_codes: JSON.stringify(updatedBackupCodes) | ||||
|         } | ||||
|       }); | ||||
|       verified = true; | ||||
|   | ||||
| @@ -27,70 +27,70 @@ function App() { | ||||
|           <Routes> | ||||
|         <Route path="/login" element={<Login />} /> | ||||
|         <Route path="/" element={ | ||||
|           <ProtectedRoute requirePermission="canViewDashboard"> | ||||
|           <ProtectedRoute requirePermission="can_view_dashboard"> | ||||
|             <Layout> | ||||
|               <Dashboard /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/hosts" element={ | ||||
|           <ProtectedRoute requirePermission="canViewHosts"> | ||||
|           <ProtectedRoute requirePermission="can_view_hosts"> | ||||
|             <Layout> | ||||
|               <Hosts /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/hosts/:hostId" element={ | ||||
|           <ProtectedRoute requirePermission="canViewHosts"> | ||||
|           <ProtectedRoute requirePermission="can_view_hosts"> | ||||
|             <Layout> | ||||
|               <HostDetail /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/packages" element={ | ||||
|           <ProtectedRoute requirePermission="canViewPackages"> | ||||
|           <ProtectedRoute requirePermission="can_view_packages"> | ||||
|             <Layout> | ||||
|               <Packages /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/repositories" element={ | ||||
|           <ProtectedRoute requirePermission="canViewHosts"> | ||||
|           <ProtectedRoute requirePermission="can_view_hosts"> | ||||
|             <Layout> | ||||
|               <Repositories /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/repositories/:repositoryId" element={ | ||||
|           <ProtectedRoute requirePermission="canViewHosts"> | ||||
|           <ProtectedRoute requirePermission="can_view_hosts"> | ||||
|             <Layout> | ||||
|               <RepositoryDetail /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/users" element={ | ||||
|           <ProtectedRoute requirePermission="canViewUsers"> | ||||
|           <ProtectedRoute requirePermission="can_view_users"> | ||||
|             <Layout> | ||||
|               <Users /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/permissions" element={ | ||||
|           <ProtectedRoute requirePermission="canManageSettings"> | ||||
|           <ProtectedRoute requirePermission="can_manage_settings"> | ||||
|             <Layout> | ||||
|               <Permissions /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/settings" element={ | ||||
|           <ProtectedRoute requirePermission="canManageSettings"> | ||||
|           <ProtectedRoute requirePermission="can_manage_settings"> | ||||
|             <Layout> | ||||
|               <Settings /> | ||||
|             </Layout> | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/options" element={ | ||||
|           <ProtectedRoute requirePermission="canManageHosts"> | ||||
|           <ProtectedRoute requirePermission="can_manage_hosts"> | ||||
|             <Layout> | ||||
|               <Options /> | ||||
|             </Layout> | ||||
| @@ -104,7 +104,7 @@ function App() { | ||||
|           </ProtectedRoute> | ||||
|         } /> | ||||
|         <Route path="/packages/:packageId" element={ | ||||
|           <ProtectedRoute requirePermission="canViewPackages"> | ||||
|           <ProtectedRoute requirePermission="can_view_packages"> | ||||
|             <Layout> | ||||
|               <PackageDetail /> | ||||
|             </Layout> | ||||
|   | ||||
| @@ -46,11 +46,11 @@ const Layout = ({ children }) => { | ||||
|   const userMenuRef = useRef(null) | ||||
|  | ||||
|   // Fetch dashboard stats for the "Last updated" info | ||||
|   const { data: stats, refetch } = useQuery({ | ||||
|   const { data: stats, refetch, isFetching } = useQuery({ | ||||
|     queryKey: ['dashboardStats'], | ||||
|     queryFn: () => dashboardAPI.getStats().then(res => res.data), | ||||
|     refetchInterval: 60000, // Refresh every minute | ||||
|     staleTime: 30000, // Consider data stale after 30 seconds | ||||
|     staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   // Fetch version info | ||||
| @@ -477,10 +477,11 @@ const Layout = ({ children }) => { | ||||
|                       <span className="truncate">Updated: {formatRelativeTimeShort(stats.lastUpdated)}</span> | ||||
|                       <button | ||||
|                         onClick={() => refetch()} | ||||
|                         className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded flex-shrink-0" | ||||
|                         disabled={isFetching} | ||||
|                         className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded flex-shrink-0 disabled:opacity-50" | ||||
|                         title="Refresh data" | ||||
|                       > | ||||
|                         <RefreshCw className="h-3 w-3" /> | ||||
|                         <RefreshCw className={`h-3 w-3 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|                       </button> | ||||
|                       {versionInfo && ( | ||||
|                         <span className="text-xs text-secondary-400 dark:text-secondary-500 flex-shrink-0"> | ||||
| @@ -516,10 +517,11 @@ const Layout = ({ children }) => { | ||||
|                   <div className="flex flex-col items-center py-1 border-t border-secondary-200 dark:border-secondary-700"> | ||||
|                     <button | ||||
|                       onClick={() => refetch()} | ||||
|                       className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded" | ||||
|                       disabled={isFetching} | ||||
|                       className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded disabled:opacity-50" | ||||
|                       title={`Refresh data - Updated: ${formatRelativeTimeShort(stats.lastUpdated)}`} | ||||
|                     > | ||||
|                       <RefreshCw className="h-3 w-3" /> | ||||
|                       <RefreshCw className={`h-3 w-3 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|                     </button> | ||||
|                     {versionInfo && ( | ||||
|                       <span className="text-xs text-secondary-400 dark:text-secondary-500 mt-1"> | ||||
|   | ||||
| @@ -15,6 +15,7 @@ export const AuthProvider = ({ children }) => { | ||||
|   const [token, setToken] = useState(null) | ||||
|   const [permissions, setPermissions] = useState(null) | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
|   const [permissionsLoading, setPermissionsLoading] = useState(false) | ||||
|  | ||||
|   // Initialize auth state from localStorage | ||||
|   useEffect(() => { | ||||
| @@ -42,20 +43,17 @@ export const AuthProvider = ({ children }) => { | ||||
|     setIsLoading(false) | ||||
|   }, []) | ||||
|  | ||||
|   // Periodically refresh permissions when user is logged in | ||||
|   // Refresh permissions when user logs in (no automatic refresh) | ||||
|   useEffect(() => { | ||||
|     if (token && user) { | ||||
|       // Refresh permissions every 30 seconds | ||||
|       const interval = setInterval(() => { | ||||
|         refreshPermissions() | ||||
|       }, 30000) | ||||
|  | ||||
|       return () => clearInterval(interval) | ||||
|       // Only refresh permissions once when user logs in | ||||
|       refreshPermissions() | ||||
|     } | ||||
|   }, [token, user]) | ||||
|  | ||||
|   const fetchPermissions = async (authToken) => { | ||||
|     try { | ||||
|       setPermissionsLoading(true) | ||||
|       const response = await fetch('/api/v1/permissions/user-permissions', { | ||||
|         headers: { | ||||
|           'Authorization': `Bearer ${authToken}`, | ||||
| @@ -74,6 +72,8 @@ export const AuthProvider = ({ children }) => { | ||||
|     } catch (error) { | ||||
|       console.error('Error fetching permissions:', error) | ||||
|       return null | ||||
|     } finally { | ||||
|       setPermissionsLoading(false) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -199,25 +199,29 @@ export const AuthProvider = ({ children }) => { | ||||
|  | ||||
|   // Permission checking functions | ||||
|   const hasPermission = (permission) => { | ||||
|     // If permissions are still loading, return false to show loading state | ||||
|     if (permissionsLoading) { | ||||
|       return false | ||||
|     } | ||||
|     return permissions?.[permission] === true | ||||
|   } | ||||
|  | ||||
|   const canViewDashboard = () => hasPermission('canViewDashboard') | ||||
|   const canViewHosts = () => hasPermission('canViewHosts') | ||||
|   const canManageHosts = () => hasPermission('canManageHosts') | ||||
|   const canViewPackages = () => hasPermission('canViewPackages') | ||||
|   const canManagePackages = () => hasPermission('canManagePackages') | ||||
|   const canViewUsers = () => hasPermission('canViewUsers') | ||||
|   const canManageUsers = () => hasPermission('canManageUsers') | ||||
|   const canViewReports = () => hasPermission('canViewReports') | ||||
|   const canExportData = () => hasPermission('canExportData') | ||||
|   const canManageSettings = () => hasPermission('canManageSettings') | ||||
|   const canViewDashboard = () => hasPermission('can_view_dashboard') | ||||
|   const canViewHosts = () => hasPermission('can_view_hosts') | ||||
|   const canManageHosts = () => hasPermission('can_manage_hosts') | ||||
|   const canViewPackages = () => hasPermission('can_view_packages') | ||||
|   const canManagePackages = () => hasPermission('can_manage_packages') | ||||
|   const canViewUsers = () => hasPermission('can_view_users') | ||||
|   const canManageUsers = () => hasPermission('can_manage_users') | ||||
|   const canViewReports = () => hasPermission('can_view_reports') | ||||
|   const canExportData = () => hasPermission('can_export_data') | ||||
|   const canManageSettings = () => hasPermission('can_manage_settings') | ||||
|  | ||||
|   const value = { | ||||
|     user, | ||||
|     token, | ||||
|     permissions, | ||||
|     isLoading, | ||||
|     isLoading: isLoading || permissionsLoading, | ||||
|     login, | ||||
|     logout, | ||||
|     updateProfile, | ||||
|   | ||||
| @@ -19,7 +19,8 @@ export const UpdateNotificationProvider = ({ children }) => { | ||||
|   const { data: updateData, isLoading, error } = useQuery({ | ||||
|     queryKey: ['updateCheck'], | ||||
|     queryFn: () => versionAPI.checkUpdates().then(res => res.data), | ||||
|     refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes | ||||
|     staleTime: 10 * 60 * 1000, // Data stays fresh for 10 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|     retry: 1 | ||||
|   }) | ||||
|  | ||||
|   | ||||
| @@ -89,11 +89,11 @@ const Dashboard = () => { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const { data: stats, isLoading, error, refetch } = useQuery({ | ||||
|   const { data: stats, isLoading, error, refetch, isFetching } = useQuery({ | ||||
|     queryKey: ['dashboardStats'], | ||||
|     queryFn: () => dashboardAPI.getStats().then(res => res.data), | ||||
|       refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute | ||||
|       staleTime: 120000, // Consider data stale after 2 minutes | ||||
|     staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   // Fetch settings to get the agent update interval | ||||
| @@ -579,6 +579,26 @@ const Dashboard = () => { | ||||
|  | ||||
|   return ( | ||||
|     <div className="space-y-6"> | ||||
|       {/* Page Header */} | ||||
|       <div className="flex items-center justify-between"> | ||||
|         <div> | ||||
|           <h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Dashboard</h1> | ||||
|           <p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1"> | ||||
|             Overview of your PatchMon infrastructure | ||||
|           </p> | ||||
|         </div> | ||||
|         <div className="flex items-center gap-3"> | ||||
|           <button | ||||
|             onClick={() => refetch()} | ||||
|             disabled={isFetching} | ||||
|             className="btn-outline flex items-center gap-2" | ||||
|             title="Refresh dashboard data" | ||||
|           > | ||||
|             <RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|             {isFetching ? 'Refreshing...' : 'Refresh'} | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {/* Dynamically Rendered Cards - Unified Order */} | ||||
|       {(() => { | ||||
|   | ||||
| @@ -46,15 +46,25 @@ const HostDetail = () => { | ||||
|   const [isEditingFriendlyName, setIsEditingFriendlyName] = useState(false) | ||||
|   const [editedFriendlyName, setEditedFriendlyName] = useState('') | ||||
|   const [showAllUpdates, setShowAllUpdates] = useState(false) | ||||
|   const [activeTab, setActiveTab] = useState('host') | ||||
|   const [activeTab, setActiveTab] = useState(() => { | ||||
|     // Restore tab state from localStorage | ||||
|     const savedTab = localStorage.getItem(`host-detail-tab-${hostId}`) | ||||
|     return savedTab || 'host' | ||||
|   }) | ||||
|    | ||||
|   const { data: host, isLoading, error, refetch } = useQuery({ | ||||
|   const { data: host, isLoading, error, refetch, isFetching } = useQuery({ | ||||
|     queryKey: ['host', hostId], | ||||
|     queryFn: () => dashboardAPI.getHostDetail(hostId).then(res => res.data), | ||||
|     refetchInterval: 60000, | ||||
|     staleTime: 30000, | ||||
|     staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   // Save tab state to localStorage when it changes | ||||
|   const handleTabChange = (tabName) => { | ||||
|     setActiveTab(tabName) | ||||
|     localStorage.setItem(`host-detail-tab-${hostId}`, tabName) | ||||
|   } | ||||
|  | ||||
|   // Auto-show credentials modal for new/pending hosts | ||||
|   React.useEffect(() => { | ||||
|     if (host && host.status === 'pending') { | ||||
| @@ -72,7 +82,7 @@ const HostDetail = () => { | ||||
|  | ||||
|   // Toggle auto-update mutation | ||||
|   const toggleAutoUpdateMutation = useMutation({ | ||||
|     mutationFn: (autoUpdate) => adminHostsAPI.toggleAutoUpdate(hostId, autoUpdate).then(res => res.data), | ||||
|     mutationFn: (auto_update) => adminHostsAPI.toggleAutoUpdate(hostId, auto_update).then(res => res.data), | ||||
|     onSuccess: () => { | ||||
|       queryClient.invalidateQueries(['host', hostId]) | ||||
|       queryClient.invalidateQueries(['hosts']) | ||||
| @@ -88,7 +98,7 @@ const HostDetail = () => { | ||||
|   }) | ||||
|  | ||||
|   const handleDeleteHost = async () => { | ||||
|     if (window.confirm(`Are you sure you want to delete host "${host.friendlyName}"? This action cannot be undone.`)) { | ||||
|     if (window.confirm(`Are you sure you want to delete host "${host.friendly_name}"? This action cannot be undone.`)) { | ||||
|       try { | ||||
|         await deleteHostMutation.mutateAsync(hostId) | ||||
|       } catch (error) { | ||||
| @@ -178,7 +188,7 @@ const HostDetail = () => { | ||||
|     return 'Up to Date' | ||||
|   } | ||||
|  | ||||
|   const isStale = new Date() - new Date(host.lastUpdate) > 24 * 60 * 60 * 1000 | ||||
|   const isStale = new Date() - new Date(host.last_update) > 24 * 60 * 60 * 1000 | ||||
|  | ||||
|   return ( | ||||
|     <div className="h-screen flex flex-col"> | ||||
| @@ -188,18 +198,18 @@ const HostDetail = () => { | ||||
|           <Link to="/hosts" className="text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200"> | ||||
|             <ArrowLeft className="h-5 w-5" /> | ||||
|           </Link> | ||||
|           <h1 className="text-xl font-semibold text-secondary-900 dark:text-white">{host.friendlyName}</h1> | ||||
|           {host.systemUptime && ( | ||||
|           <h1 className="text-xl font-semibold text-secondary-900 dark:text-white">{host.friendly_name}</h1> | ||||
|           {host.system_uptime && ( | ||||
|             <div className="flex items-center gap-1 text-sm text-secondary-600 dark:text-secondary-400"> | ||||
|               <Clock className="h-4 w-4" /> | ||||
|               <span className="text-xs font-medium">Uptime:</span> | ||||
|               <span>{host.systemUptime}</span> | ||||
|               <span>{host.system_uptime}</span> | ||||
|             </div> | ||||
|           )} | ||||
|           <div className="flex items-center gap-1 text-sm text-secondary-600 dark:text-secondary-400"> | ||||
|             <Clock className="h-4 w-4" /> | ||||
|             <span className="text-xs font-medium">Last updated:</span> | ||||
|             <span>{formatRelativeTime(host.lastUpdate)}</span> | ||||
|             <span>{formatRelativeTime(host.last_update)}</span> | ||||
|           </div> | ||||
|           <div className={`flex items-center gap-2 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(isStale, host.stats.outdatedPackages > 0)}`}> | ||||
|             {getStatusIcon(isStale, host.stats.outdatedPackages > 0)} | ||||
| @@ -207,6 +217,15 @@ const HostDetail = () => { | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="flex items-center gap-2"> | ||||
|           <button | ||||
|             onClick={() => refetch()} | ||||
|             disabled={isFetching} | ||||
|             className="btn-outline flex items-center gap-2 text-sm" | ||||
|             title="Refresh host data" | ||||
|           > | ||||
|             <RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|             {isFetching ? 'Refreshing...' : 'Refresh'} | ||||
|           </button> | ||||
|           <button | ||||
|             onClick={() => setShowCredentialsModal(true)} | ||||
|             className="btn-outline flex items-center gap-2 text-sm" | ||||
| @@ -232,7 +251,7 @@ const HostDetail = () => { | ||||
|           <div className="card"> | ||||
|             <div className="flex border-b border-secondary-200 dark:border-secondary-600"> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('host')} | ||||
|                 onClick={() => handleTabChange('host')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'host'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
| @@ -242,17 +261,7 @@ const HostDetail = () => { | ||||
|                 Host Info | ||||
|               </button> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('hardware')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'hardware'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
|                     : 'text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300' | ||||
|                 }`} | ||||
|               > | ||||
|                 Hardware | ||||
|               </button> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('network')} | ||||
|                 onClick={() => handleTabChange('network')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'network'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
| @@ -262,7 +271,7 @@ const HostDetail = () => { | ||||
|                 Network | ||||
|               </button> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('system')} | ||||
|                 onClick={() => handleTabChange('system')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'system'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
| @@ -272,17 +281,17 @@ const HostDetail = () => { | ||||
|                 System | ||||
|               </button> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('monitoring')} | ||||
|                 onClick={() => handleTabChange('monitoring')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'monitoring'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
|                     : 'text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300' | ||||
|                 }`} | ||||
|               > | ||||
|                 Resource Monitor | ||||
|                 Resource | ||||
|               </button> | ||||
|               <button  | ||||
|                 onClick={() => setActiveTab('history')} | ||||
|                 onClick={() => handleTabChange('history')} | ||||
|                 className={`px-4 py-2 text-sm font-medium ${ | ||||
|                   activeTab === 'history'  | ||||
|                     ? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'  | ||||
| @@ -301,7 +310,7 @@ const HostDetail = () => { | ||||
|                     <div> | ||||
|                       <p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1">Friendly Name</p> | ||||
|                       <InlineEdit | ||||
|                         value={host.friendlyName} | ||||
|                         value={host.friendly_name} | ||||
|                         onSave={(newName) => updateFriendlyNameMutation.mutate(newName)} | ||||
|                         placeholder="Enter friendly name..." | ||||
|                         maxLength={100} | ||||
| @@ -341,8 +350,8 @@ const HostDetail = () => { | ||||
|                     <div> | ||||
|                       <p className="text-xs text-secondary-500 dark:text-secondary-300">Operating System</p> | ||||
|                       <div className="flex items-center gap-2"> | ||||
|                         <OSIcon osType={host.osType} className="h-4 w-4" /> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.osType} {host.osVersion}</p> | ||||
|                         <OSIcon osType={host.os_type} className="h-4 w-4" /> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.os_type} {host.os_version}</p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                      | ||||
| @@ -356,29 +365,29 @@ const HostDetail = () => { | ||||
|                      | ||||
|                     <div> | ||||
|                       <p className="text-xs text-secondary-500 dark:text-secondary-300">Last Update</p> | ||||
|                       <p className="font-medium text-secondary-900 dark:text-white text-sm">{formatRelativeTime(host.lastUpdate)}</p> | ||||
|                       <p className="font-medium text-secondary-900 dark:text-white text-sm">{formatRelativeTime(host.last_update)}</p> | ||||
|                     </div> | ||||
|                      | ||||
|                     {host.agentVersion && ( | ||||
|                     {host.agent_version && ( | ||||
|                       <div className="flex items-center justify-between"> | ||||
|                         <div> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">Agent Version</p> | ||||
|                           <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.agentVersion}</p> | ||||
|                           <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.agent_version}</p> | ||||
|                         </div> | ||||
|                         <div className="flex items-center gap-2"> | ||||
|                           <span className="text-xs text-secondary-500 dark:text-secondary-300">Auto-update</span> | ||||
|                           <button | ||||
|                             onClick={() => toggleAutoUpdateMutation.mutate(!host.autoUpdate)} | ||||
|                             onClick={() => toggleAutoUpdateMutation.mutate(!host.auto_update)} | ||||
|                             disabled={toggleAutoUpdateMutation.isPending} | ||||
|                             className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${ | ||||
|                               host.autoUpdate  | ||||
|                               host.auto_update  | ||||
|                                 ? 'bg-primary-600 dark:bg-primary-500'  | ||||
|                                 : 'bg-secondary-200 dark:bg-secondary-600' | ||||
|                             }`} | ||||
|                           > | ||||
|                             <span | ||||
|                               className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${ | ||||
|                                 host.autoUpdate ? 'translate-x-5' : 'translate-x-1' | ||||
|                                 host.auto_update ? 'translate-x-5' : 'translate-x-1' | ||||
|                               }`} | ||||
|                             /> | ||||
|                           </button> | ||||
| @@ -389,88 +398,34 @@ const HostDetail = () => { | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {/* Hardware Information */} | ||||
|               {activeTab === 'hardware' && (host.cpuModel || host.ramInstalled || host.diskDetails) && ( | ||||
|                 <div className="space-y-4"> | ||||
|                   <div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> | ||||
|                     {host.cpuModel && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Model</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpuModel}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.cpuCores && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Cores</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpuCores}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.ramInstalled && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">RAM Installed</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.ramInstalled} GB</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.swapSize !== undefined && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">Swap Size</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.swapSize} GB</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </div> | ||||
|                    | ||||
|                   {host.diskDetails && Array.isArray(host.diskDetails) && host.diskDetails.length > 0 && ( | ||||
|                     <div className="pt-4 border-t border-secondary-200 dark:border-secondary-600"> | ||||
|                       <h4 className="text-sm font-medium text-secondary-900 dark:text-white mb-3">Disk Details</h4> | ||||
|                       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | ||||
|                         {host.diskDetails.map((disk, index) => ( | ||||
|                           <div key={index} className="bg-secondary-50 dark:bg-secondary-700 p-3 rounded-lg"> | ||||
|                             <div className="flex items-center gap-2 mb-1"> | ||||
|                               <HardDrive className="h-4 w-4 text-secondary-500" /> | ||||
|                               <span className="font-medium text-secondary-900 dark:text-white text-sm">{disk.name}</span> | ||||
|                             </div> | ||||
|                             <p className="text-xs text-secondary-600 dark:text-secondary-300">Size: {disk.size}</p> | ||||
|                             {disk.mountpoint && ( | ||||
|                               <p className="text-xs text-secondary-600 dark:text-secondary-300">Mount: {disk.mountpoint}</p> | ||||
|                             )} | ||||
|                           </div> | ||||
|                         ))} | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {/* Network Information */} | ||||
|               {activeTab === 'network' && (host.gatewayIp || host.dnsServers || host.networkInterfaces) && ( | ||||
|               {activeTab === 'network' && (host.gateway_ip || host.dns_servers || host.network_interfaces) && ( | ||||
|                 <div className="space-y-4"> | ||||
|                   <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | ||||
|                     {host.gatewayIp && ( | ||||
|                     {host.gateway_ip && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">Gateway IP</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.gatewayIp}</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.gateway_ip}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.dnsServers && Array.isArray(host.dnsServers) && host.dnsServers.length > 0 && ( | ||||
|                     {host.dns_servers && Array.isArray(host.dns_servers) && host.dns_servers.length > 0 && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">DNS Servers</p> | ||||
|                         <div className="space-y-1"> | ||||
|                           {host.dnsServers.map((dns, index) => ( | ||||
|                           {host.dns_servers.map((dns, index) => ( | ||||
|                             <p key={index} className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{dns}</p> | ||||
|                           ))} | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.networkInterfaces && Array.isArray(host.networkInterfaces) && host.networkInterfaces.length > 0 && ( | ||||
|                     {host.network_interfaces && Array.isArray(host.network_interfaces) && host.network_interfaces.length > 0 && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">Network Interfaces</p> | ||||
|                         <div className="space-y-1"> | ||||
|                           {host.networkInterfaces.map((iface, index) => ( | ||||
|                           {host.network_interfaces.map((iface, index) => ( | ||||
|                             <p key={index} className="font-medium text-secondary-900 dark:text-white text-sm">{iface.name}</p> | ||||
|                           ))} | ||||
|                         </div> | ||||
| @@ -481,7 +436,7 @@ const HostDetail = () => { | ||||
|               )} | ||||
|  | ||||
|               {/* System Information */} | ||||
|               {activeTab === 'system' && (host.kernelVersion || host.selinuxStatus || host.architecture) && ( | ||||
|               {activeTab === 'system' && (host.kernel_version || host.selinux_status || host.architecture) && ( | ||||
|                 <div className="space-y-4"> | ||||
|                   <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | ||||
|                     {host.architecture && ( | ||||
| @@ -491,24 +446,24 @@ const HostDetail = () => { | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.kernelVersion && ( | ||||
|                     {host.kernel_version && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">Kernel Version</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.kernelVersion}</p> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.kernel_version}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {host.selinuxStatus && ( | ||||
|                     {host.selinux_status && ( | ||||
|                       <div> | ||||
|                         <p className="text-xs text-secondary-500 dark:text-secondary-300">SELinux Status</p> | ||||
|                         <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${ | ||||
|                           host.selinuxStatus === 'enabled'  | ||||
|                           host.selinux_status === 'enabled'  | ||||
|                             ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' | ||||
|                             : host.selinuxStatus === 'permissive' | ||||
|                             : host.selinux_status === 'permissive' | ||||
|                             ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' | ||||
|                             : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' | ||||
|                         }`}> | ||||
|                           {host.selinuxStatus} | ||||
|                           {host.selinux_status} | ||||
|                         </span> | ||||
|                       </div> | ||||
|                     )} | ||||
| @@ -518,22 +473,15 @@ const HostDetail = () => { | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {/* Empty state for tabs with no data */} | ||||
|               {activeTab === 'hardware' && !(host.cpuModel || host.ramInstalled || host.diskDetails) && ( | ||||
|                 <div className="text-center py-8"> | ||||
|                   <Cpu className="h-8 w-8 text-secondary-400 mx-auto mb-2" /> | ||||
|                   <p className="text-sm text-secondary-500 dark:text-secondary-300">No hardware information available</p> | ||||
|                 </div> | ||||
|               )} | ||||
|                | ||||
|               {activeTab === 'network' && !(host.gatewayIp || host.dnsServers || host.networkInterfaces) && ( | ||||
|               {activeTab === 'network' && !(host.gateway_ip || host.dns_servers || host.network_interfaces) && ( | ||||
|                 <div className="text-center py-8"> | ||||
|                   <Wifi className="h-8 w-8 text-secondary-400 mx-auto mb-2" /> | ||||
|                   <p className="text-sm text-secondary-500 dark:text-secondary-300">No network information available</p> | ||||
|                 </div> | ||||
|               )} | ||||
|                | ||||
|               {activeTab === 'system' && !(host.kernelVersion || host.selinuxStatus || host.architecture) && ( | ||||
|               {activeTab === 'system' && !(host.kernel_version || host.selinux_status || host.architecture) && ( | ||||
|                 <div className="text-center py-8"> | ||||
|                   <Terminal className="h-8 w-8 text-secondary-400 mx-auto mb-2" /> | ||||
|                   <p className="text-sm text-secondary-500 dark:text-secondary-300">No system information available</p> | ||||
| @@ -541,35 +489,143 @@ const HostDetail = () => { | ||||
|               )} | ||||
|  | ||||
|               {/* System Monitoring */} | ||||
|               {activeTab === 'monitoring' && host.loadAverage && Array.isArray(host.loadAverage) && host.loadAverage.length > 0 && ( | ||||
|                 <div className="space-y-4"> | ||||
|               {activeTab === 'monitoring' && ( | ||||
|                 <div className="space-y-6"> | ||||
|                   {/* System Overview */} | ||||
|                   <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | ||||
|                     <div> | ||||
|                       <p className="text-xs text-secondary-500 dark:text-secondary-300">Load Average</p> | ||||
|                       <p className="font-medium text-secondary-900 dark:text-white text-sm"> | ||||
|                         {host.loadAverage.map((load, index) => ( | ||||
|                           <span key={index}> | ||||
|                             {load.toFixed(2)} | ||||
|                             {index < host.loadAverage.length - 1 && ', '} | ||||
|                           </span> | ||||
|                     {/* System Uptime */} | ||||
|                     {host.system_uptime && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <Clock className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">System Uptime</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.system_uptime}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {/* CPU Model */} | ||||
|                     {host.cpu_model && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <Cpu className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Model</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpu_model}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {/* CPU Cores */} | ||||
|                     {host.cpu_cores && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <Cpu className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Cores</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpu_cores}</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {/* RAM Installed */} | ||||
|                     {host.ram_installed && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <MemoryStick className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">RAM Installed</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.ram_installed} GB</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {/* Swap Size */} | ||||
|                     {host.swap_size !== undefined && host.swap_size !== null && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <MemoryStick className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">Swap Size</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm">{host.swap_size} GB</p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                      | ||||
|                     {/* Load Average */} | ||||
|                     {host.load_average && Array.isArray(host.load_average) && host.load_average.length > 0 && host.load_average.some(load => load != null) && ( | ||||
|                       <div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg"> | ||||
|                         <div className="flex items-center gap-2 mb-2"> | ||||
|                           <Activity className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                           <p className="text-xs text-secondary-500 dark:text-secondary-300">Load Average</p> | ||||
|                         </div> | ||||
|                         <p className="font-medium text-secondary-900 dark:text-white text-sm"> | ||||
|                           {host.load_average.filter(load => load != null).map((load, index) => ( | ||||
|                             <span key={index}> | ||||
|                               {typeof load === 'number' ? load.toFixed(2) : String(load)} | ||||
|                               {index < host.load_average.filter(load => load != null).length - 1 && ', '} | ||||
|                             </span> | ||||
|                           ))} | ||||
|                         </p> | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </div> | ||||
|                    | ||||
|                   {/* Disk Information */} | ||||
|                   {host.disk_details && Array.isArray(host.disk_details) && host.disk_details.length > 0 && ( | ||||
|                     <div className="pt-4 border-t border-secondary-200 dark:border-secondary-600"> | ||||
|                       <h4 className="text-sm font-medium text-secondary-900 dark:text-white mb-3 flex items-center gap-2"> | ||||
|                         <HardDrive className="h-4 w-4 text-primary-600 dark:text-primary-400" /> | ||||
|                         Disk Usage | ||||
|                       </h4> | ||||
|                       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> | ||||
|                         {host.disk_details.map((disk, index) => ( | ||||
|                           <div key={index} className="bg-secondary-50 dark:bg-secondary-700 p-3 rounded-lg"> | ||||
|                             <div className="flex items-center gap-2 mb-2"> | ||||
|                               <HardDrive className="h-4 w-4 text-secondary-500" /> | ||||
|                               <span className="font-medium text-secondary-900 dark:text-white text-sm">{disk.name || `Disk ${index + 1}`}</span> | ||||
|                             </div> | ||||
|                             {disk.size && ( | ||||
|                               <p className="text-xs text-secondary-600 dark:text-secondary-300 mb-1">Size: {disk.size}</p> | ||||
|                             )} | ||||
|                             {disk.mountpoint && ( | ||||
|                               <p className="text-xs text-secondary-600 dark:text-secondary-300 mb-1">Mount: {disk.mountpoint}</p> | ||||
|                             )} | ||||
|                             {disk.usage && typeof disk.usage === 'number' && ( | ||||
|                               <div className="mt-2"> | ||||
|                                 <div className="flex justify-between text-xs text-secondary-600 dark:text-secondary-300 mb-1"> | ||||
|                                   <span>Usage</span> | ||||
|                                   <span>{disk.usage}%</span> | ||||
|                                 </div> | ||||
|                                 <div className="w-full bg-secondary-200 dark:bg-secondary-600 rounded-full h-2"> | ||||
|                                   <div  | ||||
|                                     className="bg-primary-600 dark:bg-primary-400 h-2 rounded-full transition-all duration-300" | ||||
|                                     style={{ width: `${Math.min(Math.max(disk.usage, 0), 100)}%` }} | ||||
|                                   ></div> | ||||
|                                 </div> | ||||
|                               </div> | ||||
|                             )} | ||||
|                           </div> | ||||
|                         ))} | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   )} | ||||
|                    | ||||
|                   {/* No Data State */} | ||||
|                   {!host.system_uptime && !host.cpu_model && !host.cpu_cores && !host.ram_installed && host.swap_size === undefined &&  | ||||
|                    (!host.load_average || !Array.isArray(host.load_average) || host.load_average.length === 0 || !host.load_average.some(load => load != null)) &&  | ||||
|                    (!host.disk_details || !Array.isArray(host.disk_details) || host.disk_details.length === 0) && ( | ||||
|                     <div className="text-center py-8"> | ||||
|                       <Monitor className="h-8 w-8 text-secondary-400 mx-auto mb-2" /> | ||||
|                       <p className="text-sm text-secondary-500 dark:text-secondary-300">No monitoring data available</p> | ||||
|                       <p className="text-xs text-secondary-400 dark:text-secondary-400 mt-1"> | ||||
|                         Monitoring data will appear once the agent collects system information | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {activeTab === 'monitoring' && (!host.loadAverage || !Array.isArray(host.loadAverage) || host.loadAverage.length === 0) && ( | ||||
|                 <div className="text-center py-8"> | ||||
|                   <Monitor className="h-8 w-8 text-secondary-400 mx-auto mb-2" /> | ||||
|                   <p className="text-sm text-secondary-500 dark:text-secondary-300">No monitoring data available</p> | ||||
|                   )} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {/* Update History */} | ||||
|               {activeTab === 'history' && ( | ||||
|                 <div className="overflow-x-auto"> | ||||
|                   {host.updateHistory?.length > 0 ? ( | ||||
|                   {host.update_history?.length > 0 ? ( | ||||
|                     <> | ||||
|                       <table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600"> | ||||
|                         <thead className="bg-secondary-50 dark:bg-secondary-700"> | ||||
| @@ -589,7 +645,7 @@ const HostDetail = () => { | ||||
|                           </tr> | ||||
|                         </thead> | ||||
|                         <tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600"> | ||||
|                           {(showAllUpdates ? host.updateHistory : host.updateHistory.slice(0, 5)).map((update, index) => ( | ||||
|                           {(showAllUpdates ? host.update_history : host.update_history.slice(0, 5)).map((update, index) => ( | ||||
|                             <tr key={update.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-700"> | ||||
|                               <td className="px-4 py-2 whitespace-nowrap"> | ||||
|                                 <div className="flex items-center gap-1.5"> | ||||
| @@ -607,14 +663,14 @@ const HostDetail = () => { | ||||
|                                 {formatDate(update.timestamp)} | ||||
|                               </td> | ||||
|                               <td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white"> | ||||
|                                 {update.packagesCount} | ||||
|                                 {update.packages_count} | ||||
|                               </td> | ||||
|                               <td className="px-4 py-2 whitespace-nowrap"> | ||||
|                                 {update.securityCount > 0 ? ( | ||||
|                                 {update.security_count > 0 ? ( | ||||
|                                   <div className="flex items-center gap-1"> | ||||
|                                     <Shield className="h-3 w-3 text-danger-600" /> | ||||
|                                     <span className="text-xs text-danger-600 font-medium"> | ||||
|                                       {update.securityCount} | ||||
|                                       {update.security_count} | ||||
|                                     </span> | ||||
|                                   </div> | ||||
|                                 ) : ( | ||||
| @@ -626,7 +682,7 @@ const HostDetail = () => { | ||||
|                         </tbody> | ||||
|                       </table> | ||||
|                        | ||||
|                       {host.updateHistory.length > 5 && ( | ||||
|                       {host.update_history.length > 5 && ( | ||||
|                         <div className="px-4 py-2 border-t border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700"> | ||||
|                           <button | ||||
|                             onClick={() => setShowAllUpdates(!showAllUpdates)} | ||||
| @@ -640,7 +696,7 @@ const HostDetail = () => { | ||||
|                             ) : ( | ||||
|                               <> | ||||
|                                 <ChevronDown className="h-3 w-3" /> | ||||
|                                 Show All ({host.updateHistory.length} total) | ||||
|                                 Show All ({host.update_history.length} total) | ||||
|                               </> | ||||
|                             )} | ||||
|                           </button> | ||||
| @@ -746,7 +802,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { | ||||
|   } | ||||
|  | ||||
|   const getSetupCommands = () => { | ||||
|     return `# Run this on the target host: ${host?.friendlyName} | ||||
|     return `# Run this on the target host: ${host?.friendly_name} | ||||
|  | ||||
| echo "🔄 Setting up PatchMon agent..." | ||||
|  | ||||
| @@ -788,7 +844,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|     <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | ||||
|       <div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto"> | ||||
|         <div className="flex justify-between items-center mb-4"> | ||||
|           <h3 className="text-lg font-medium text-secondary-900 dark:text-white">Host Setup - {host.friendlyName}</h3> | ||||
|           <h3 className="text-lg font-medium text-secondary-900 dark:text-white">Host Setup - {host.friendly_name}</h3> | ||||
|           <button onClick={onClose} className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"> | ||||
|             <X className="h-5 w-5" /> | ||||
|           </button> | ||||
| @@ -1069,7 +1125,7 @@ const DeleteConfirmationModal = ({ host, isOpen, onClose, onConfirm, isLoading } | ||||
|         <div className="mb-6"> | ||||
|           <p className="text-secondary-700 dark:text-secondary-300"> | ||||
|             Are you sure you want to delete the host{' '} | ||||
|             <span className="font-semibold">"{host.friendlyName}"</span>? | ||||
|             <span className="font-semibold">"{host.friendly_name}"</span>? | ||||
|           </p> | ||||
|           <div className="mt-3 p-3 bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md"> | ||||
|             <p className="text-sm text-danger-800 dark:text-danger-200"> | ||||
|   | ||||
| @@ -38,7 +38,7 @@ import InlineGroupEdit from '../components/InlineGroupEdit' | ||||
| // Add Host Modal Component | ||||
| const AddHostModal = ({ isOpen, onClose, onSuccess }) => { | ||||
|   const [formData, setFormData] = useState({ | ||||
|     friendlyName: '', | ||||
|     friendly_name: '', | ||||
|     hostGroupId: '' | ||||
|   }) | ||||
|   const [isSubmitting, setIsSubmitting] = useState(false) | ||||
| @@ -62,7 +62,7 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => { | ||||
|       const response = await adminHostsAPI.create(formData) | ||||
|       console.log('Host created successfully:', response.data) | ||||
|       onSuccess(response.data) | ||||
|       setFormData({ friendlyName: '', hostGroupId: '' }) | ||||
|       setFormData({ friendly_name: '', hostGroupId: '' }) | ||||
|       onClose() | ||||
|     } catch (err) { | ||||
|       console.error('Full error object:', err) | ||||
| @@ -105,8 +105,8 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => { | ||||
|             <input | ||||
|               type="text" | ||||
|               required | ||||
|               value={formData.friendlyName} | ||||
|               onChange={(e) => setFormData({ ...formData, friendlyName: e.target.value })} | ||||
|               value={formData.friendly_name} | ||||
|               onChange={(e) => setFormData({ ...formData, friendly_name: e.target.value })} | ||||
|               className="block w-full px-3 py-2.5 text-base border-2 border-secondary-300 dark:border-secondary-600 rounded-lg shadow-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white transition-all duration-200" | ||||
|               placeholder="server.example.com" | ||||
|             /> | ||||
| @@ -252,7 +252,7 @@ echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo | ||||
|  | ||||
|       fullSetup: `#!/bin/bash | ||||
| # Complete PatchMon Agent Setup Script | ||||
| # Run this on the target host: ${host?.friendlyName} | ||||
| # Run this on the target host: ${host?.friendly_name} | ||||
|  | ||||
| echo "🔄 Setting up PatchMon agent..." | ||||
|  | ||||
| @@ -295,7 +295,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|     <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | ||||
|       <div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto"> | ||||
|         <div className="flex justify-between items-center mb-4"> | ||||
|           <h3 className="text-lg font-medium text-secondary-900">Host Setup - {host.friendlyName}</h3> | ||||
|           <h3 className="text-lg font-medium text-secondary-900">Host Setup - {host.friendly_name}</h3> | ||||
|           <button onClick={onClose} className="text-secondary-400 hover:text-secondary-600"> | ||||
|             <X className="h-5 w-5" /> | ||||
|           </button> | ||||
| @@ -351,7 +351,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|             <div className="bg-green-50 border border-green-200 rounded-md p-4"> | ||||
|               <h4 className="text-sm font-medium text-green-800 mb-2">🚀 One-Line Installation</h4> | ||||
|               <p className="text-sm text-green-700"> | ||||
|                 Copy and paste this single command on <strong>{host.friendlyName}</strong> to install and configure the PatchMon agent automatically. | ||||
|                 Copy and paste this single command on <strong>{host.friendly_name}</strong> to install and configure the PatchMon agent automatically. | ||||
|               </p> | ||||
|             </div> | ||||
|  | ||||
| @@ -378,7 +378,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|               <ul className="text-sm text-blue-700 space-y-1"> | ||||
|                 <li>• Downloads the PatchMon installation script</li> | ||||
|                 <li>• Installs the agent to <code>/usr/local/bin/patchmon-agent.sh</code></li> | ||||
|                 <li>• Configures API credentials for <strong>{host.friendlyName}</strong></li> | ||||
|                 <li>• Configures API credentials for <strong>{host.friendly_name}</strong></li> | ||||
|                 <li>• Tests the connection to PatchMon server</li> | ||||
|                 <li>• Sends initial package data</li> | ||||
|                 <li>• Sets up hourly automatic updates via crontab</li> | ||||
| @@ -444,7 +444,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|             <div className="bg-amber-50 border border-amber-200 rounded-md p-4"> | ||||
|               <h4 className="text-sm font-medium text-amber-800 mb-2">⚠️ Security Note</h4> | ||||
|               <p className="text-sm text-amber-700"> | ||||
|                 Keep these credentials secure. They provide access to update package information for <strong>{host.friendlyName}</strong> only. | ||||
|                 Keep these credentials secure. They provide access to update package information for <strong>{host.friendly_name}</strong> only. | ||||
|               </p> | ||||
|             </div> | ||||
|           </div> | ||||
| @@ -455,7 +455,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|             <div className="bg-blue-50 border border-blue-200 rounded-md p-4"> | ||||
|               <h4 className="text-sm font-medium text-blue-800 mb-2">📋 Step-by-Step Setup</h4> | ||||
|               <p className="text-sm text-blue-700"> | ||||
|                 Follow these commands on <strong>{host.friendlyName}</strong> to install and configure the PatchMon agent. | ||||
|                 Follow these commands on <strong>{host.friendly_name}</strong> to install and configure the PatchMon agent. | ||||
|               </p> | ||||
|             </div> | ||||
|  | ||||
| @@ -552,7 +552,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|             <div className="bg-green-50 border border-green-200 rounded-md p-4"> | ||||
|               <h4 className="text-sm font-medium text-green-800 mb-2">🚀 Automated Setup</h4> | ||||
|               <p className="text-sm text-green-700"> | ||||
|                 Copy this complete setup script to <strong>{host.friendlyName}</strong> and run it to automatically install and configure everything. | ||||
|                 Copy this complete setup script to <strong>{host.friendly_name}</strong> and run it to automatically install and configure everything. | ||||
|               </p> | ||||
|             </div> | ||||
|  | ||||
| @@ -573,7 +573,7 @@ echo "   - View logs: tail -f /var/log/patchmon-agent.log"` | ||||
|               <div className="mt-3 text-sm text-secondary-600"> | ||||
|                 <p><strong>Usage:</strong></p> | ||||
|                 <p>1. Copy the script above</p> | ||||
|                 <p>2. Save it to a file on {host.friendlyName} (e.g., <code>setup-patchmon.sh</code>)</p> | ||||
|                 <p>2. Save it to a file on {host.friendly_name} (e.g., <code>setup-patchmon.sh</code>)</p> | ||||
|                 <p>3. Run: <code>chmod +x setup-patchmon.sh && sudo ./setup-patchmon.sh</code></p> | ||||
|               </div> | ||||
|             </div> | ||||
| @@ -666,65 +666,38 @@ const Hosts = () => { | ||||
|       { id: 'ip', label: 'IP Address', visible: false, order: 2 }, | ||||
|       { id: 'group', label: 'Group', visible: true, order: 3 }, | ||||
|       { id: 'os', label: 'OS', visible: true, order: 4 }, | ||||
|       { id: 'osVersion', label: 'OS Version', visible: false, order: 5 }, | ||||
|       { id: 'agentVersion', label: 'Agent Version', visible: true, order: 6 }, | ||||
|       { id: 'autoUpdate', label: 'Auto-update', visible: true, order: 7 }, | ||||
|       { id: 'os_version', label: 'OS Version', visible: false, order: 5 }, | ||||
|       { id: 'agent_version', label: 'Agent Version', visible: true, order: 6 }, | ||||
|       { id: 'auto_update', label: 'Auto-update', visible: true, order: 7 }, | ||||
|       { id: 'status', label: 'Status', visible: true, order: 8 }, | ||||
|       { id: 'updates', label: 'Updates', visible: true, order: 9 }, | ||||
|       { id: 'lastUpdate', label: 'Last Update', visible: true, order: 10 }, | ||||
|       { id: 'last_update', label: 'Last Update', visible: true, order: 10 }, | ||||
|       { id: 'actions', label: 'Actions', visible: true, order: 11 } | ||||
|     ] | ||||
|  | ||||
|     const saved = localStorage.getItem('hosts-column-config') | ||||
|     if (saved) { | ||||
|       const savedConfig = JSON.parse(saved) | ||||
|       try { | ||||
|         const savedConfig = JSON.parse(saved) | ||||
|          | ||||
|       // Check if agentVersion column exists in saved config | ||||
|       const hasAgentVersion = savedConfig.some(col => col.id === 'agentVersion') | ||||
|       const hasAutoUpdate = savedConfig.some(col => col.id === 'autoUpdate') | ||||
|         // Check if we have old camelCase column IDs that need to be migrated | ||||
|         const hasOldColumns = savedConfig.some(col =>  | ||||
|           col.id === 'agentVersion' || col.id === 'autoUpdate' || col.id === 'osVersion' || col.id === 'lastUpdate' | ||||
|         ) | ||||
|          | ||||
|       let needsUpdate = false | ||||
|       let updatedConfig = [...savedConfig] | ||||
|        | ||||
|       if (!hasAgentVersion) { | ||||
|         // Add agentVersion column to saved config | ||||
|         const agentVersionColumn = { id: 'agentVersion', label: 'Agent Version', visible: true, order: 6 } | ||||
|          | ||||
|         // Insert agentVersion column at the correct position | ||||
|         updatedConfig = updatedConfig.map(col => { | ||||
|           if (col.order >= 6) { | ||||
|             return { ...col, order: col.order + 1 } | ||||
|           } | ||||
|           return col | ||||
|         }) | ||||
|          | ||||
|         updatedConfig.push(agentVersionColumn) | ||||
|         needsUpdate = true | ||||
|         if (hasOldColumns) { | ||||
|           // Clear the old configuration and use the default snake_case configuration | ||||
|           localStorage.removeItem('hosts-column-config') | ||||
|           return defaultConfig | ||||
|         } else { | ||||
|           // Use the existing configuration | ||||
|           return savedConfig | ||||
|         } | ||||
|       } catch (error) { | ||||
|         // If there's an error parsing the config, clear it and use default | ||||
|         localStorage.removeItem('hosts-column-config') | ||||
|         return defaultConfig | ||||
|       } | ||||
|        | ||||
|       if (!hasAutoUpdate) { | ||||
|         // Add autoUpdate column to saved config | ||||
|         const autoUpdateColumn = { id: 'autoUpdate', label: 'Auto-update', visible: true, order: 7 } | ||||
|          | ||||
|         // Insert autoUpdate column at the correct position | ||||
|         updatedConfig = updatedConfig.map(col => { | ||||
|           if (col.order >= 7) { | ||||
|             return { ...col, order: col.order + 1 } | ||||
|           } | ||||
|           return col | ||||
|         }) | ||||
|          | ||||
|         updatedConfig.push(autoUpdateColumn) | ||||
|         needsUpdate = true | ||||
|       } | ||||
|        | ||||
|       if (needsUpdate) { | ||||
|         updatedConfig.sort((a, b) => a.order - b.order) | ||||
|         localStorage.setItem('hosts-column-config', JSON.stringify(updatedConfig)) | ||||
|         return updatedConfig | ||||
|       } | ||||
|        | ||||
|       return savedConfig | ||||
|     } | ||||
|      | ||||
|     return defaultConfig | ||||
| @@ -732,11 +705,11 @@ const Hosts = () => { | ||||
|    | ||||
|   const queryClient = useQueryClient() | ||||
|  | ||||
|   const { data: hosts, isLoading, error, refetch } = useQuery({ | ||||
|   const { data: hosts, isLoading, error, refetch, isFetching } = useQuery({ | ||||
|     queryKey: ['hosts'], | ||||
|     queryFn: () => dashboardAPI.getHosts().then(res => res.data), | ||||
|       refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute | ||||
|       staleTime: 120000, // Consider data stale after 2 minutes | ||||
|     staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   const { data: hostGroups } = useQuery({ | ||||
| @@ -776,7 +749,7 @@ const Hosts = () => { | ||||
|  | ||||
|   // Toggle auto-update mutation | ||||
|   const toggleAutoUpdateMutation = useMutation({ | ||||
|     mutationFn: ({ hostId, autoUpdate }) => adminHostsAPI.toggleAutoUpdate(hostId, autoUpdate).then(res => res.data), | ||||
|     mutationFn: ({ hostId, auto_update }) => adminHostsAPI.toggleAutoUpdate(hostId, auto_update).then(res => res.data), | ||||
|     onSuccess: () => { | ||||
|       queryClient.invalidateQueries(['hosts']) | ||||
|     } | ||||
| @@ -859,9 +832,9 @@ const Hosts = () => { | ||||
|     let filtered = hosts.filter(host => { | ||||
|       // Search filter | ||||
|       const matchesSearch = searchTerm === '' ||  | ||||
|         host.friendlyName.toLowerCase().includes(searchTerm.toLowerCase()) || | ||||
|         host.friendly_name.toLowerCase().includes(searchTerm.toLowerCase()) || | ||||
|         host.ip?.toLowerCase().includes(searchTerm.toLowerCase()) || | ||||
|         host.osType?.toLowerCase().includes(searchTerm.toLowerCase()) | ||||
|         host.os_type?.toLowerCase().includes(searchTerm.toLowerCase()) | ||||
|  | ||||
|       // Group filter | ||||
|       const matchesGroup = groupFilter === 'all' ||  | ||||
| @@ -872,7 +845,7 @@ const Hosts = () => { | ||||
|       const matchesStatus = statusFilter === 'all' || (host.effectiveStatus || host.status) === statusFilter | ||||
|  | ||||
|       // OS filter | ||||
|       const matchesOs = osFilter === 'all' || host.osType?.toLowerCase() === osFilter.toLowerCase() | ||||
|       const matchesOs = osFilter === 'all' || host.os_type?.toLowerCase() === osFilter.toLowerCase() | ||||
|  | ||||
|       // URL filter for hosts needing updates, inactive hosts, or up-to-date hosts | ||||
|       const filter = searchParams.get('filter') | ||||
| @@ -893,8 +866,8 @@ const Hosts = () => { | ||||
|        | ||||
|       switch (sortField) { | ||||
|         case 'friendlyName': | ||||
|           aValue = a.friendlyName.toLowerCase() | ||||
|           bValue = b.friendlyName.toLowerCase() | ||||
|           aValue = a.friendly_name.toLowerCase() | ||||
|           bValue = b.friendly_name.toLowerCase() | ||||
|           break | ||||
|         case 'hostname': | ||||
|           aValue = a.hostname?.toLowerCase() || 'zzz_no_hostname' | ||||
| @@ -909,16 +882,16 @@ const Hosts = () => { | ||||
|           bValue = b.hostGroup?.name || 'zzz_ungrouped' | ||||
|           break | ||||
|         case 'os': | ||||
|           aValue = a.osType?.toLowerCase() || 'zzz_unknown' | ||||
|           bValue = b.osType?.toLowerCase() || 'zzz_unknown' | ||||
|           aValue = a.os_type?.toLowerCase() || 'zzz_unknown' | ||||
|           bValue = b.os_type?.toLowerCase() || 'zzz_unknown' | ||||
|           break | ||||
|         case 'osVersion': | ||||
|           aValue = a.osVersion?.toLowerCase() || 'zzz_unknown' | ||||
|           bValue = b.osVersion?.toLowerCase() || 'zzz_unknown' | ||||
|         case 'os_version': | ||||
|           aValue = a.os_version?.toLowerCase() || 'zzz_unknown' | ||||
|           bValue = b.os_version?.toLowerCase() || 'zzz_unknown' | ||||
|           break | ||||
|         case 'agentVersion': | ||||
|           aValue = a.agentVersion?.toLowerCase() || 'zzz_no_version' | ||||
|           bValue = b.agentVersion?.toLowerCase() || 'zzz_no_version' | ||||
|         case 'agent_version': | ||||
|           aValue = a.agent_version?.toLowerCase() || 'zzz_no_version' | ||||
|           bValue = b.agent_version?.toLowerCase() || 'zzz_no_version' | ||||
|           break | ||||
|         case 'status': | ||||
|           aValue = a.effectiveStatus || a.status | ||||
| @@ -928,9 +901,9 @@ const Hosts = () => { | ||||
|           aValue = a.updatesCount || 0 | ||||
|           bValue = b.updatesCount || 0 | ||||
|           break | ||||
|         case 'lastUpdate': | ||||
|           aValue = new Date(a.lastUpdate) | ||||
|           bValue = new Date(b.lastUpdate) | ||||
|         case 'last_update': | ||||
|           aValue = new Date(a.last_update) | ||||
|           bValue = new Date(b.last_update) | ||||
|           break | ||||
|         default: | ||||
|           aValue = a[sortField] | ||||
| @@ -962,7 +935,7 @@ const Hosts = () => { | ||||
|           groupKey = (host.effectiveStatus || host.status).charAt(0).toUpperCase() + (host.effectiveStatus || host.status).slice(1) | ||||
|           break | ||||
|         case 'os': | ||||
|           groupKey = host.osType || 'Unknown' | ||||
|           groupKey = host.os_type || 'Unknown' | ||||
|           break | ||||
|         default: | ||||
|           groupKey = 'All Hosts' | ||||
| @@ -1022,10 +995,10 @@ const Hosts = () => { | ||||
|       { id: 'ip', label: 'IP Address', visible: false, order: 3 }, | ||||
|       { id: 'group', label: 'Group', visible: true, order: 4 }, | ||||
|       { id: 'os', label: 'OS', visible: true, order: 5 }, | ||||
|       { id: 'osVersion', label: 'OS Version', visible: false, order: 6 }, | ||||
|       { id: 'os_version', label: 'OS Version', visible: false, order: 6 }, | ||||
|       { id: 'status', label: 'Status', visible: true, order: 7 }, | ||||
|       { id: 'updates', label: 'Updates', visible: true, order: 8 }, | ||||
|       { id: 'lastUpdate', label: 'Last Update', visible: true, order: 9 }, | ||||
|       { id: 'last_update', label: 'Last Update', visible: true, order: 9 }, | ||||
|       { id: 'actions', label: 'Actions', visible: true, order: 10 } | ||||
|     ] | ||||
|     updateColumnConfig(defaultConfig) | ||||
| @@ -1055,7 +1028,7 @@ const Hosts = () => { | ||||
|       case 'host': | ||||
|         return ( | ||||
|           <InlineEdit | ||||
|             value={host.friendlyName} | ||||
|             value={host.friendly_name} | ||||
|             onSave={(newName) => updateFriendlyNameMutation.mutate({ hostId: host.id, friendlyName: newName })} | ||||
|             placeholder="Enter friendly name..." | ||||
|             maxLength={100} | ||||
| @@ -1101,30 +1074,30 @@ const Hosts = () => { | ||||
|       case 'os': | ||||
|         return ( | ||||
|           <div className="flex items-center gap-2 text-sm text-secondary-900 dark:text-white"> | ||||
|             <OSIcon osType={host.osType} className="h-4 w-4" /> | ||||
|             <span>{host.osType}</span> | ||||
|             <OSIcon osType={host.os_type} className="h-4 w-4" /> | ||||
|             <span>{host.os_type}</span> | ||||
|           </div> | ||||
|         ) | ||||
|       case 'osVersion': | ||||
|       case 'os_version': | ||||
|         return ( | ||||
|           <div className="text-sm text-secondary-900 dark:text-white"> | ||||
|             {host.osVersion || 'N/A'} | ||||
|             {host.os_version || 'N/A'} | ||||
|           </div> | ||||
|         ) | ||||
|       case 'agentVersion': | ||||
|       case 'agent_version': | ||||
|         return ( | ||||
|           <div className="text-sm text-secondary-900 dark:text-white"> | ||||
|             {host.agentVersion || 'N/A'} | ||||
|             {host.agent_version || 'N/A'} | ||||
|           </div> | ||||
|         ) | ||||
|       case 'autoUpdate': | ||||
|       case 'auto_update': | ||||
|         return ( | ||||
|           <span className={`text-sm font-medium ${ | ||||
|             host.autoUpdate  | ||||
|             host.auto_update  | ||||
|               ? 'text-green-600 dark:text-green-400'  | ||||
|               : 'text-red-600 dark:text-red-400' | ||||
|           }`}> | ||||
|             {host.autoUpdate ? 'Yes' : 'No'} | ||||
|             {host.auto_update ? 'Yes' : 'No'} | ||||
|           </span> | ||||
|         ) | ||||
|       case 'status': | ||||
| @@ -1139,10 +1112,10 @@ const Hosts = () => { | ||||
|             {host.updatesCount || 0} | ||||
|           </div> | ||||
|         ) | ||||
|       case 'lastUpdate': | ||||
|       case 'last_update': | ||||
|         return ( | ||||
|           <div className="text-sm text-secondary-500 dark:text-secondary-300"> | ||||
|             {formatRelativeTime(host.lastUpdate)} | ||||
|             {formatRelativeTime(host.last_update)} | ||||
|           </div> | ||||
|         ) | ||||
|       case 'actions': | ||||
| @@ -1260,6 +1233,33 @@ const Hosts = () => { | ||||
|  | ||||
|   return ( | ||||
|     <div className="space-y-6"> | ||||
|       {/* Page Header */} | ||||
|       <div className="flex items-center justify-between"> | ||||
|         <div> | ||||
|           <h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Hosts</h1> | ||||
|           <p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1"> | ||||
|             Manage and monitor your connected hosts | ||||
|           </p> | ||||
|         </div> | ||||
|         <div className="flex items-center gap-3"> | ||||
|           <button | ||||
|             onClick={() => refetch()} | ||||
|             disabled={isFetching} | ||||
|             className="btn-outline flex items-center gap-2" | ||||
|             title="Refresh hosts data" | ||||
|           > | ||||
|             <RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|             {isFetching ? 'Refreshing...' : 'Refresh'} | ||||
|           </button> | ||||
|           <button | ||||
|             onClick={() => setShowAddModal(true)} | ||||
|             className="btn-primary flex items-center gap-2" | ||||
|           > | ||||
|             <Plus className="h-4 w-4" /> | ||||
|             Add Host | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {/* Stats Summary */} | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-4"> | ||||
| @@ -1550,23 +1550,23 @@ const Hosts = () => { | ||||
|                             {column.label} | ||||
|                             {getSortIcon('os')} | ||||
|                           </button> | ||||
|                         ) : column.id === 'osVersion' ? ( | ||||
|                         ) : column.id === 'os_version' ? ( | ||||
|                           <button | ||||
|                             onClick={() => handleSort('osVersion')} | ||||
|                             onClick={() => handleSort('os_version')} | ||||
|                             className="flex items-center gap-2 hover:text-secondary-700" | ||||
|                           > | ||||
|                             {column.label} | ||||
|                             {getSortIcon('osVersion')} | ||||
|                             {getSortIcon('os_version')} | ||||
|                           </button> | ||||
|                         ) : column.id === 'agentVersion' ? ( | ||||
|                         ) : column.id === 'agent_version' ? ( | ||||
|                           <button | ||||
|                             onClick={() => handleSort('agentVersion')} | ||||
|                             onClick={() => handleSort('agent_version')} | ||||
|                             className="flex items-center gap-2 hover:text-secondary-700" | ||||
|                           > | ||||
|                             {column.label} | ||||
|                             {getSortIcon('agentVersion')} | ||||
|                             {getSortIcon('agent_version')} | ||||
|                           </button> | ||||
|                         ) : column.id === 'autoUpdate' ? ( | ||||
|                         ) : column.id === 'auto_update' ? ( | ||||
|                           <div className="flex items-center gap-2 font-normal text-xs text-secondary-500 dark:text-secondary-300 normal-case tracking-wider"> | ||||
|                             {column.label} | ||||
|                           </div> | ||||
| @@ -1586,13 +1586,13 @@ const Hosts = () => { | ||||
|                             {column.label} | ||||
|                             {getSortIcon('updates')} | ||||
|                           </button> | ||||
|                         ) : column.id === 'lastUpdate' ? ( | ||||
|                         ) : column.id === 'last_update' ? ( | ||||
|                           <button | ||||
|                             onClick={() => handleSort('lastUpdate')} | ||||
|                             onClick={() => handleSort('last_update')} | ||||
|                             className="flex items-center gap-2 hover:text-secondary-700" | ||||
|                           > | ||||
|                             {column.label} | ||||
|                             {getSortIcon('lastUpdate')} | ||||
|                             {getSortIcon('last_update')} | ||||
|                           </button> | ||||
|                         ) : ( | ||||
|                           column.label | ||||
| @@ -1679,7 +1679,7 @@ const BulkAssignModal = ({ selectedHosts, hosts, onClose, onAssign, isLoading }) | ||||
|  | ||||
|   const selectedHostNames = hosts | ||||
|     .filter(host => selectedHosts.includes(host.id)) | ||||
|     .map(host => host.friendlyName) | ||||
|     .map(host => host.friendly_name) | ||||
|  | ||||
|   const handleSubmit = (e) => { | ||||
|     e.preventDefault() | ||||
|   | ||||
| @@ -7,7 +7,8 @@ import { | ||||
|   Server,  | ||||
|   Users, | ||||
|   AlertTriangle, | ||||
|   CheckCircle | ||||
|   CheckCircle, | ||||
|   Settings | ||||
| } from 'lucide-react' | ||||
| import { hostGroupsAPI } from '../utils/api' | ||||
|  | ||||
| @@ -214,7 +215,7 @@ const Options = () => { | ||||
|  | ||||
|   const renderComingSoonTab = (tabName) => ( | ||||
|     <div className="text-center py-12"> | ||||
|       <SettingsIcon className="h-12 w-12 text-secondary-400 mx-auto mb-4" /> | ||||
|       <Settings className="h-12 w-12 text-secondary-400 mx-auto mb-4" /> | ||||
|       <h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-2"> | ||||
|         {tabName} Coming Soon | ||||
|       </h3> | ||||
|   | ||||
| @@ -98,19 +98,19 @@ const Packages = () => { | ||||
|     } | ||||
|   }, [searchParams]) | ||||
|  | ||||
|   const { data: packages, isLoading, error, refetch } = useQuery({ | ||||
|   const { data: packages, isLoading, error, refetch, isFetching } = useQuery({ | ||||
|     queryKey: ['packages'], | ||||
|     queryFn: () => dashboardAPI.getPackages().then(res => res.data), | ||||
|     refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute | ||||
|     staleTime: 120000, // Consider data stale after 2 minutes | ||||
|     staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   // Fetch hosts data to get total packages count | ||||
|   const { data: hosts } = useQuery({ | ||||
|     queryKey: ['hosts'], | ||||
|     queryFn: () => dashboardAPI.getHosts().then(res => res.data), | ||||
|     refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute | ||||
|     staleTime: 120000, // Consider data stale after 2 minutes | ||||
|     staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes | ||||
|     refetchOnWindowFocus: false, // Don't refetch when window regains focus | ||||
|   }) | ||||
|  | ||||
|   // Filter and sort packages | ||||
| @@ -330,6 +330,26 @@ const Packages = () => { | ||||
|  | ||||
|   return ( | ||||
|     <div className="h-[calc(100vh-7rem)] flex flex-col overflow-hidden"> | ||||
|       {/* Page Header */} | ||||
|       <div className="flex items-center justify-between mb-6"> | ||||
|         <div> | ||||
|           <h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Packages</h1> | ||||
|           <p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1"> | ||||
|             Manage package updates and security patches | ||||
|           </p> | ||||
|         </div> | ||||
|         <div className="flex items-center gap-3"> | ||||
|           <button | ||||
|             onClick={() => refetch()} | ||||
|             disabled={isFetching} | ||||
|             className="btn-outline flex items-center gap-2" | ||||
|             title="Refresh packages data" | ||||
|           > | ||||
|             <RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} /> | ||||
|             {isFetching ? 'Refreshing...' : 'Refresh'} | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       {/* Summary Stats */} | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6 flex-shrink-0"> | ||||
|   | ||||
| @@ -148,8 +148,8 @@ const Repositories = () => { | ||||
|                          (filterType === 'insecure' && !isSecure); | ||||
|        | ||||
|       const matchesStatus = filterStatus === 'all' || | ||||
|                            (filterStatus === 'active' && repo.isActive === true) || | ||||
|                            (filterStatus === 'inactive' && repo.isActive === false); | ||||
|                            (filterStatus === 'active' && repo.is_active === true) || | ||||
|                            (filterStatus === 'inactive' && repo.is_active === false); | ||||
|        | ||||
|       console.log('Filter results:', { | ||||
|         matchesSearch, | ||||
| @@ -171,8 +171,8 @@ const Repositories = () => { | ||||
|         aValue = a.isSecure ? 'Secure' : 'Insecure'; | ||||
|         bValue = b.isSecure ? 'Secure' : 'Insecure'; | ||||
|       } else if (sortField === 'status') { | ||||
|         aValue = a.isActive ? 'Active' : 'Inactive'; | ||||
|         bValue = b.isActive ? 'Active' : 'Inactive'; | ||||
|         aValue = a.is_active ? 'Active' : 'Inactive'; | ||||
|         bValue = b.is_active ? 'Active' : 'Inactive'; | ||||
|       } | ||||
|  | ||||
|       if (typeof aValue === 'string') { | ||||
| @@ -426,11 +426,11 @@ const Repositories = () => { | ||||
|       case 'status': | ||||
|         return ( | ||||
|           <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ | ||||
|             repo.isActive | ||||
|             repo.is_active | ||||
|               ? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' | ||||
|               : 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300' | ||||
|           }`}> | ||||
|             {repo.isActive ? 'Active' : 'Inactive'} | ||||
|             {repo.is_active ? 'Active' : 'Inactive'} | ||||
|           </span> | ||||
|         ) | ||||
|       case 'hostCount': | ||||
|   | ||||
| @@ -45,7 +45,7 @@ const RepositoryDetail = () => { | ||||
|     setFormData({ | ||||
|       name: repository.name, | ||||
|       description: repository.description || '', | ||||
|       isActive: repository.isActive, | ||||
|       is_active: repository.is_active, | ||||
|       priority: repository.priority || '' | ||||
|     }); | ||||
|     setEditMode(true); | ||||
| @@ -139,11 +139,11 @@ const RepositoryDetail = () => { | ||||
|                 {repository.name} | ||||
|               </h1> | ||||
|               <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ | ||||
|                 repository.isActive | ||||
|                 repository.is_active | ||||
|                   ? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300' | ||||
|                   : 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300' | ||||
|               }`}> | ||||
|                 {repository.isActive ? 'Active' : 'Inactive'} | ||||
|                 {repository.is_active ? 'Active' : 'Inactive'} | ||||
|               </span> | ||||
|             </div> | ||||
|             <p className="text-secondary-500 dark:text-secondary-300 mt-1"> | ||||
| @@ -228,12 +228,12 @@ const RepositoryDetail = () => { | ||||
|               <div className="flex items-center"> | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   id="isActive" | ||||
|                   checked={formData.isActive} | ||||
|                   onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })} | ||||
|                   id="is_active" | ||||
|                   checked={formData.is_active} | ||||
|                   onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })} | ||||
|                   className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded" | ||||
|                 /> | ||||
|                 <label htmlFor="isActive" className="ml-2 block text-sm text-secondary-900 dark:text-white"> | ||||
|                 <label htmlFor="is_active" className="ml-2 block text-sm text-secondary-900 dark:text-white"> | ||||
|                   Repository is active | ||||
|                 </label> | ||||
|               </div> | ||||
| @@ -295,7 +295,7 @@ const RepositoryDetail = () => { | ||||
|                   <div className="flex items-center mt-1"> | ||||
|                     <Calendar className="h-4 w-4 text-secondary-400 mr-2" /> | ||||
|                     <span className="text-secondary-900 dark:text-white"> | ||||
|                       {new Date(repository.createdAt).toLocaleDateString()} | ||||
|                       {new Date(repository.created_at).toLocaleDateString()} | ||||
|                     </span> | ||||
|                   </div> | ||||
|                 </div> | ||||
| @@ -339,7 +339,7 @@ const RepositoryDetail = () => { | ||||
|                         to={`/hosts/${hostRepo.host.id}`} | ||||
|                         className="text-primary-600 hover:text-primary-700 font-medium" | ||||
|                       > | ||||
|                         {hostRepo.host.friendlyName} | ||||
|                         {hostRepo.host.friendly_name} | ||||
|                       </Link> | ||||
|                       <div className="flex items-center gap-4 text-sm text-secondary-500 dark:text-secondary-400 mt-1"> | ||||
|                         <span>IP: {hostRepo.host.ip}</span> | ||||
|   | ||||
| @@ -732,7 +732,7 @@ const Settings = () => { | ||||
|                         </div> | ||||
|                       )} | ||||
|                       <p className="text-xs text-secondary-400 dark:text-secondary-400 mt-1"> | ||||
|                         Created: {new Date(version.createdAt).toLocaleDateString()} | ||||
|                         Created: {new Date(version.created_at).toLocaleDateString()} | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|   | ||||
| @@ -140,7 +140,7 @@ const Users = () => { | ||||
|                           <Shield className="h-3 w-3 mr-1" /> | ||||
|                           {user.role.charAt(0).toUpperCase() + user.role.slice(1).replace('_', ' ')} | ||||
|                         </span> | ||||
|                         {user.isActive ? ( | ||||
|                         {user.is_active ? ( | ||||
|                           <CheckCircle className="ml-2 h-4 w-4 text-green-500" /> | ||||
|                         ) : ( | ||||
|                           <XCircle className="ml-2 h-4 w-4 text-red-500" /> | ||||
| @@ -152,11 +152,11 @@ const Users = () => { | ||||
|                       </div> | ||||
|                       <div className="flex items-center mt-1 text-sm text-secondary-500 dark:text-secondary-300"> | ||||
|                         <Calendar className="h-4 w-4 mr-1" /> | ||||
|                         Created: {new Date(user.createdAt).toLocaleDateString()} | ||||
|                         {user.lastLogin && ( | ||||
|                         Created: {new Date(user.created_at).toLocaleDateString()} | ||||
|                         {user.last_login && ( | ||||
|                           <> | ||||
|                             <span className="mx-2">•</span> | ||||
|                             Last login: {new Date(user.lastLogin).toLocaleDateString()} | ||||
|                             Last login: {new Date(user.last_login).toLocaleDateString()} | ||||
|                           </> | ||||
|                         )} | ||||
|                       </div> | ||||
| @@ -174,11 +174,11 @@ const Users = () => { | ||||
|                       onClick={() => handleResetPassword(user)} | ||||
|                       className="text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-300 disabled:text-gray-300 disabled:cursor-not-allowed" | ||||
|                       title={ | ||||
|                         !user.isActive  | ||||
|                         !user.is_active  | ||||
|                           ? "Cannot reset password for inactive user" | ||||
|                           : "Reset password" | ||||
|                       } | ||||
|                       disabled={!user.isActive} | ||||
|                       disabled={!user.is_active} | ||||
|                     > | ||||
|                       <Key className="h-4 w-4" /> | ||||
|                     </button> | ||||
| @@ -394,7 +394,7 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => { | ||||
|     username: user?.username || '', | ||||
|     email: user?.email || '', | ||||
|     role: user?.role || 'user', | ||||
|     isActive: user?.isActive ?? true | ||||
|     is_active: user?.is_active ?? true | ||||
|   }) | ||||
|   const [isLoading, setIsLoading] = useState(false) | ||||
|   const [error, setError] = useState('') | ||||
| @@ -486,8 +486,8 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => { | ||||
|           <div className="flex items-center"> | ||||
|             <input | ||||
|               type="checkbox" | ||||
|               name="isActive" | ||||
|               checked={formData.isActive} | ||||
|               name="is_active" | ||||
|               checked={formData.is_active} | ||||
|               onChange={handleInputChange} | ||||
|               className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded" | ||||
|             /> | ||||
|   | ||||
| @@ -63,8 +63,8 @@ export const adminHostsAPI = { | ||||
|   regenerateCredentials: (hostId) => api.post(`/hosts/${hostId}/regenerate-credentials`), | ||||
|   updateGroup: (hostId, hostGroupId) => api.put(`/hosts/${hostId}/group`, { hostGroupId }), | ||||
|   bulkUpdateGroup: (hostIds, hostGroupId) => api.put('/hosts/bulk/group', { hostIds, hostGroupId }), | ||||
|   toggleAutoUpdate: (hostId, autoUpdate) => api.patch(`/hosts/${hostId}/auto-update`, { autoUpdate }), | ||||
|   updateFriendlyName: (hostId, friendlyName) => api.patch(`/hosts/${hostId}/friendly-name`, { friendlyName }) | ||||
|   toggleAutoUpdate: (hostId, autoUpdate) => api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }), | ||||
|   updateFriendlyName: (hostId, friendlyName) => api.patch(`/hosts/${hostId}/friendly-name`, { friendly_name: friendlyName }) | ||||
| } | ||||
|  | ||||
| // Host Groups API | ||||
| @@ -156,7 +156,7 @@ export const hostsAPI = { | ||||
|       'X-API-KEY': apiKey | ||||
|     } | ||||
|   }), | ||||
|   toggleAutoUpdate: (id, autoUpdate) => api.patch(`/hosts/${id}/auto-update`, { autoUpdate }) | ||||
|   toggleAutoUpdate: (id, autoUpdate) => api.patch(`/hosts/${id}/auto-update`, { auto_update: autoUpdate }) | ||||
| } | ||||
|  | ||||
| // Packages API | ||||
|   | ||||
							
								
								
									
										95
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										95
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -59,6 +59,7 @@ | ||||
|         "cors": "^2.8.5", | ||||
|         "date-fns": "^2.30.0", | ||||
|         "express": "^4.18.2", | ||||
|         "http-proxy-middleware": "^2.0.6", | ||||
|         "lucide-react": "^0.294.0", | ||||
|         "react": "^18.2.0", | ||||
|         "react-chartjs-2": "^5.2.0", | ||||
| @@ -1773,6 +1774,24 @@ | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/@types/http-proxy": { | ||||
|       "version": "1.17.16", | ||||
|       "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", | ||||
|       "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "24.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", | ||||
|       "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "undici-types": "~7.12.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/prop-types": { | ||||
|       "version": "15.7.15", | ||||
|       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", | ||||
| @@ -2249,7 +2268,6 @@ | ||||
|       "version": "3.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", | ||||
|       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "fill-range": "^7.1.1" | ||||
| @@ -3495,6 +3513,12 @@ | ||||
|         "node": ">= 0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/eventemitter3": { | ||||
|       "version": "4.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", | ||||
|       "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/express": { | ||||
|       "version": "4.21.2", | ||||
|       "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", | ||||
| @@ -3640,7 +3664,6 @@ | ||||
|       "version": "7.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", | ||||
|       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "to-regex-range": "^5.0.1" | ||||
| @@ -4152,6 +4175,44 @@ | ||||
|         "node": ">= 0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/http-proxy": { | ||||
|       "version": "1.18.1", | ||||
|       "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", | ||||
|       "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "eventemitter3": "^4.0.0", | ||||
|         "follow-redirects": "^1.0.0", | ||||
|         "requires-port": "^1.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=8.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/http-proxy-middleware": { | ||||
|       "version": "2.0.9", | ||||
|       "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", | ||||
|       "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/http-proxy": "^1.17.8", | ||||
|         "http-proxy": "^1.18.1", | ||||
|         "is-glob": "^4.0.1", | ||||
|         "is-plain-obj": "^3.0.0", | ||||
|         "micromatch": "^4.0.2" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=12.0.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@types/express": "^4.17.13" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@types/express": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/iconv-lite": { | ||||
|       "version": "0.4.24", | ||||
|       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", | ||||
| @@ -4408,7 +4469,6 @@ | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||||
|       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=0.10.0" | ||||
| @@ -4462,7 +4522,6 @@ | ||||
|       "version": "4.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", | ||||
|       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "is-extglob": "^2.1.1" | ||||
| @@ -4501,7 +4560,6 @@ | ||||
|       "version": "7.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||
|       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=0.12.0" | ||||
| @@ -4534,6 +4592,18 @@ | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/is-plain-obj": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", | ||||
|       "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/sindresorhus" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/is-regex": { | ||||
|       "version": "1.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", | ||||
| @@ -5105,7 +5175,6 @@ | ||||
|       "version": "4.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", | ||||
|       "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "braces": "^3.0.3", | ||||
| @@ -5675,7 +5744,6 @@ | ||||
|       "version": "2.3.1", | ||||
|       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", | ||||
|       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=8.6" | ||||
| @@ -6356,6 +6424,12 @@ | ||||
|       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", | ||||
|       "license": "ISC" | ||||
|     }, | ||||
|     "node_modules/requires-port": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", | ||||
|       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/resolve": { | ||||
|       "version": "2.0.0-next.5", | ||||
|       "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", | ||||
| @@ -7350,7 +7424,6 @@ | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||
|       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "is-number": "^7.0.0" | ||||
| @@ -7553,6 +7626,12 @@ | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/undici-types": { | ||||
|       "version": "7.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", | ||||
|       "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/unpipe": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user