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