🚀 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>
599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
'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>
|
|
);
|
|
} |