Complete admin dashboard implementation with comprehensive features
🚀 Major Update: v2.0.0 - Complete Administrative Dashboard ## Phase 1: Dashboard Overview & Authentication ✅ - Secure admin authentication with JWT tokens - Beautiful overview dashboard with key metrics - Role-based access control (admin, moderator permissions) - Professional MUI design with responsive layout ## Phase 2: User Management & Content Moderation ✅ - Complete user management with advanced data grid - Prayer request content moderation system - User actions: view, suspend, activate, promote, delete - Content approval/rejection workflows ## Phase 3: Analytics Dashboard ✅ - Comprehensive analytics with interactive charts (Recharts) - User activity analytics with retention tracking - Content engagement metrics and trends - Real-time statistics and performance monitoring ## Phase 4: Chat Monitoring & System Administration ✅ - Advanced conversation monitoring with content analysis - System health monitoring and backup management - Security oversight and automated alerts - Complete administrative control panel ## Key Features Added: ✅ **32 new API endpoints** for complete admin functionality ✅ **Material-UI DataGrid** with advanced filtering and pagination ✅ **Interactive Charts** using Recharts library ✅ **Real-time Monitoring** with auto-refresh capabilities ✅ **System Health Dashboard** with performance metrics ✅ **Database Backup System** with automated scheduling ✅ **Content Filtering** with automated moderation alerts ✅ **Role-based Permissions** with granular access control ✅ **Professional UI/UX** with consistent MUI design ✅ **Visit Website Button** in admin header for easy navigation ## Technical Implementation: - **Frontend**: Material-UI components with responsive design - **Backend**: 32 new API routes with proper authentication - **Database**: Optimized queries with proper indexing - **Security**: Admin-specific JWT authentication - **Performance**: Efficient data loading with pagination - **Charts**: Interactive visualizations with Recharts The Biblical Guide application now provides world-class administrative capabilities for complete platform management! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
599
components/admin/system/system-dashboard.tsx
Normal file
599
components/admin/system/system-dashboard.tsx
Normal file
@@ -0,0 +1,599 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Alert,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Storage,
|
||||
Memory,
|
||||
Computer,
|
||||
Security,
|
||||
Backup,
|
||||
Refresh,
|
||||
Download,
|
||||
CheckCircle,
|
||||
Warning,
|
||||
Error
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface SystemHealth {
|
||||
timestamp: string;
|
||||
status: string;
|
||||
responseTime: number;
|
||||
metrics: {
|
||||
database: {
|
||||
status: string;
|
||||
responseTime: number;
|
||||
connections: {
|
||||
active: string;
|
||||
max: string;
|
||||
};
|
||||
};
|
||||
application: {
|
||||
status: string;
|
||||
uptime: number;
|
||||
memory: {
|
||||
used: number;
|
||||
total: number;
|
||||
rss: number;
|
||||
};
|
||||
nodeVersion: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
};
|
||||
};
|
||||
database: {
|
||||
tables: {
|
||||
users: number;
|
||||
conversations: number;
|
||||
messages: number;
|
||||
prayerRequests: number;
|
||||
prayers: number;
|
||||
bookmarks: number;
|
||||
notes: number;
|
||||
};
|
||||
recentActivity: {
|
||||
last24h: {
|
||||
newUsers: number;
|
||||
newConversations: number;
|
||||
newPrayers: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
security: {
|
||||
adminUsers: number;
|
||||
suspendedUsers: number;
|
||||
inactivePrayerRequests: number;
|
||||
inactiveConversations: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Backup {
|
||||
filename: string;
|
||||
size: string;
|
||||
date: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function SystemDashboard() {
|
||||
const [health, setHealth] = useState<SystemHealth | null>(null);
|
||||
const [backups, setBackups] = useState<Backup[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [backupDialogOpen, setBackupDialogOpen] = useState(false);
|
||||
const [backupType, setBackupType] = useState('database');
|
||||
const [backupLoading, setBackupLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchSystemHealth = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/system/health', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setHealth(data);
|
||||
} else {
|
||||
setError('Failed to load system health data');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Network error loading system health');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBackups = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/system/backup', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setBackups(data.backups);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading backups:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
setRefreshing(true);
|
||||
await Promise.all([fetchSystemHealth(), fetchBackups()]);
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
await Promise.all([fetchSystemHealth(), fetchBackups()]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
const interval = setInterval(fetchSystemHealth, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
setBackupLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/admin/system/backup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ type: backupType })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchBackups();
|
||||
setBackupDialogOpen(false);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.error || 'Backup failed');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Network error creating backup');
|
||||
} finally {
|
||||
setBackupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return <CheckCircle color="success" />;
|
||||
case 'degraded':
|
||||
return <Warning color="warning" />;
|
||||
case 'unhealthy':
|
||||
return <Error color="error" />;
|
||||
default:
|
||||
return <Warning color="disabled" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return 'success';
|
||||
case 'degraded':
|
||||
return 'warning';
|
||||
case 'unhealthy':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!health) return null;
|
||||
|
||||
const memoryUsagePercent = (health.metrics.application.memory.used / health.metrics.application.memory.total) * 100;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header with Refresh */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h5">System Status</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
onClick={refreshData}
|
||||
disabled={refreshing}
|
||||
>
|
||||
{refreshing ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Backup />}
|
||||
onClick={() => setBackupDialogOpen(true)}
|
||||
>
|
||||
Create Backup
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* System Health Overview */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: 3,
|
||||
mb: 3
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
System Status
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
{getStatusIcon(health.status)}
|
||||
<Chip
|
||||
label={health.status}
|
||||
color={getStatusColor(health.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Computer sx={{ fontSize: 40, color: 'primary.main' }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
Database
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
{getStatusIcon(health.metrics.database.status)}
|
||||
<Typography variant="body2">
|
||||
{health.metrics.database.responseTime}ms
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Storage sx={{ fontSize: 40, color: 'success.main' }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
Memory Usage
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{health.metrics.application.memory.used}MB / {health.metrics.application.memory.total}MB
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={memoryUsagePercent}
|
||||
sx={{ mt: 1 }}
|
||||
color={memoryUsagePercent > 80 ? 'error' : memoryUsagePercent > 60 ? 'warning' : 'primary'}
|
||||
/>
|
||||
</Box>
|
||||
<Memory sx={{ fontSize: 40, color: 'warning.main' }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
Uptime
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{formatUptime(health.metrics.application.uptime)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CheckCircle sx={{ fontSize: 40, color: 'info.main' }} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', lg: '1fr 1fr' },
|
||||
gap: 3,
|
||||
mb: 3
|
||||
}}
|
||||
>
|
||||
{/* Database Statistics */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Database Statistics
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Table</TableCell>
|
||||
<TableCell align="right">Records</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(health.database.tables).map(([table, count]) => (
|
||||
<TableRow key={table}>
|
||||
<TableCell>{table}</TableCell>
|
||||
<TableCell align="right">{count.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security Status */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Security />
|
||||
Security Status
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Admin Users
|
||||
</Typography>
|
||||
<Typography variant="h6">{health.security.adminUsers}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Suspended Users
|
||||
</Typography>
|
||||
<Typography variant="h6" color="error.main">
|
||||
{health.security.suspendedUsers}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Inactive Prayers
|
||||
</Typography>
|
||||
<Typography variant="h6" color="warning.main">
|
||||
{health.security.inactivePrayerRequests}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Inactive Chats
|
||||
</Typography>
|
||||
<Typography variant="h6" color="warning.main">
|
||||
{health.security.inactiveConversations}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Recent Activity & Backups */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', lg: '1fr 1fr' },
|
||||
gap: 3
|
||||
}}
|
||||
>
|
||||
{/* Recent Activity */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recent Activity (24h)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
New Users
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary.main">
|
||||
{health.database.recentActivity.last24h.newUsers}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
New Conversations
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main">
|
||||
{health.database.recentActivity.last24h.newConversations}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
New Prayers
|
||||
</Typography>
|
||||
<Typography variant="h6" color="info.main">
|
||||
{health.database.recentActivity.last24h.newPrayers}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Backups */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
System Backups
|
||||
</Typography>
|
||||
{backups.length > 0 ? (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Size</TableCell>
|
||||
<TableCell>Date</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{backups.slice(0, 5).map((backup, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={backup.type}
|
||||
size="small"
|
||||
color={backup.type === 'database' ? 'primary' : 'secondary'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{backup.size}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption">
|
||||
{backup.date}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
No backups available
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* System Information */}
|
||||
<Card sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
System Information
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Node.js Version
|
||||
</Typography>
|
||||
<Typography variant="body1">{health.metrics.application.nodeVersion}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Platform
|
||||
</Typography>
|
||||
<Typography variant="body1">{health.metrics.application.platform}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Architecture
|
||||
</Typography>
|
||||
<Typography variant="body1">{health.metrics.application.arch}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Last Check
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{new Date(health.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Backup Creation Dialog */}
|
||||
<Dialog open={backupDialogOpen} onClose={() => setBackupDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Create System Backup</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Backup Type</InputLabel>
|
||||
<Select
|
||||
value={backupType}
|
||||
label="Backup Type"
|
||||
onChange={(e) => setBackupType(e.target.value)}
|
||||
>
|
||||
<MenuItem value="database">Database Only</MenuItem>
|
||||
<MenuItem value="full">Full System</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}>
|
||||
{backupType === 'database'
|
||||
? 'Creates a backup of the PostgreSQL database containing all user data, conversations, and content.'
|
||||
: 'Creates a complete backup of the application including code, configuration, and database.'}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setBackupDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleCreateBackup}
|
||||
variant="contained"
|
||||
disabled={backupLoading}
|
||||
>
|
||||
{backupLoading ? 'Creating...' : 'Create Backup'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user