Files
biblical-guide.com/components/admin/system/system-dashboard.tsx
Andrei 39b6899315 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>
2025-09-23 12:01:34 +00:00

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>
);
}