mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +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(() => {
 | 
			
		||||
      // Only refresh permissions once when user logs in
 | 
			
		||||
      refreshPermissions()
 | 
			
		||||
      }, 30000)
 | 
			
		||||
 | 
			
		||||
      return () => clearInterval(interval)
 | 
			
		||||
    }
 | 
			
		||||
  }, [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>
 | 
			
		||||
                    {/* 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.loadAverage.map((load, index) => (
 | 
			
		||||
                          {host.load_average.filter(load => load != null).map((load, index) => (
 | 
			
		||||
                            <span key={index}>
 | 
			
		||||
                            {load.toFixed(2)}
 | 
			
		||||
                            {index < host.loadAverage.length - 1 && ', '}
 | 
			
		||||
                              {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>
 | 
			
		||||
                  )}
 | 
			
		||||
                  
 | 
			
		||||
              {activeTab === 'monitoring' && (!host.loadAverage || !Array.isArray(host.loadAverage) || host.loadAverage.length === 0) && (
 | 
			
		||||
                  {/* 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>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              {/* 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,77 +666,50 @@ 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) {
 | 
			
		||||
      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')
 | 
			
		||||
      
 | 
			
		||||
      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 (!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
 | 
			
		||||
      }
 | 
			
		||||
        // 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'
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        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
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return defaultConfig
 | 
			
		||||
  })
 | 
			
		||||
  
 | 
			
		||||
  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