- migrate from SQLite to PostgreSQL with Drizzle ORM - implement comprehensive AdminLayout with expandable sidebar navigation - create professional dashboard with real-time charts and metrics - add advanced monitoring, reporting, and export functionality - fix menu alignment and remove non-existent pages - eliminate duplicate headers and improve UI consistency - add Tailwind CSS v3 for professional styling - expand database schema from 6 to 15 tables - implement role-based access control and API key management - create comprehensive settings, monitoring, and system info pages
432 lines
14 KiB
TypeScript
432 lines
14 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
|
import type { NextPage } from 'next'
|
|
import Head from 'next/head'
|
|
import AdminLayout from '../../../components/AdminLayout'
|
|
import {
|
|
ChartBarIcon,
|
|
ServerStackIcon,
|
|
CircleStackIcon as DatabaseIcon,
|
|
CpuChipIcon,
|
|
CircleStackIcon,
|
|
GlobeAltIcon,
|
|
ExclamationTriangleIcon,
|
|
CheckCircleIcon,
|
|
ClockIcon,
|
|
ArrowPathIcon
|
|
} from '@heroicons/react/24/outline'
|
|
import { Line, Bar } from 'react-chartjs-2'
|
|
|
|
interface RealtimeMetrics {
|
|
timestamp: string
|
|
cpu_usage: number
|
|
memory_usage: number
|
|
disk_usage: number
|
|
active_connections: number
|
|
api_requests_per_minute: number
|
|
database_queries_per_minute: number
|
|
response_time: number
|
|
error_rate: number
|
|
}
|
|
|
|
interface SystemAlert {
|
|
id: string
|
|
type: 'warning' | 'error' | 'info'
|
|
title: string
|
|
message: string
|
|
timestamp: string
|
|
resolved: boolean
|
|
}
|
|
|
|
interface ActiveUser {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
last_activity: string
|
|
ip_address: string
|
|
location: string
|
|
}
|
|
|
|
const RealtimeMonitoring: NextPage = () => {
|
|
const [metrics, setMetrics] = useState<RealtimeMetrics[]>([])
|
|
const [alerts, setAlerts] = useState<SystemAlert[]>([])
|
|
const [activeUsers, setActiveUsers] = useState<ActiveUser[]>([])
|
|
const [isConnected, setIsConnected] = useState(false)
|
|
const [autoRefresh, setAutoRefresh] = useState(true)
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (autoRefresh) {
|
|
fetchData()
|
|
intervalRef.current = setInterval(fetchData, 5000) // Update every 5 seconds
|
|
} else if (intervalRef.current) {
|
|
clearInterval(intervalRef.current)
|
|
}
|
|
|
|
return () => {
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current)
|
|
}
|
|
}
|
|
}, [autoRefresh])
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
// Fetch real-time metrics
|
|
const metricsResponse = await fetch('/api/admin/monitoring/realtime')
|
|
if (metricsResponse.ok) {
|
|
const newMetric = await metricsResponse.json()
|
|
setMetrics(prev => {
|
|
const updated = [...prev, newMetric].slice(-20) // Keep last 20 data points
|
|
return updated
|
|
})
|
|
setIsConnected(true)
|
|
}
|
|
|
|
// Fetch alerts
|
|
const alertsResponse = await fetch('/api/admin/monitoring/alerts')
|
|
if (alertsResponse.ok) {
|
|
const alertsData = await alertsResponse.json()
|
|
setAlerts(alertsData)
|
|
}
|
|
|
|
// Fetch active users
|
|
const usersResponse = await fetch('/api/admin/monitoring/active-users')
|
|
if (usersResponse.ok) {
|
|
const usersData = await usersResponse.json()
|
|
setActiveUsers(usersData)
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch monitoring data:', error)
|
|
setIsConnected(false)
|
|
}
|
|
}
|
|
|
|
const getLatestMetric = () => metrics[metrics.length - 1] || {}
|
|
|
|
const getStatusColor = (value: number, thresholds: { warning: number, critical: number }) => {
|
|
if (value >= thresholds.critical) return 'text-red-600 bg-red-100'
|
|
if (value >= thresholds.warning) return 'text-yellow-600 bg-yellow-100'
|
|
return 'text-green-600 bg-green-100'
|
|
}
|
|
|
|
// Chart configurations
|
|
const cpuChartData = {
|
|
labels: metrics.map((_, index) => `${index * 5}s ago`).reverse(),
|
|
datasets: [
|
|
{
|
|
label: 'CPU Usage %',
|
|
data: metrics.map(m => m.cpu_usage).reverse(),
|
|
borderColor: 'rgb(239, 68, 68)',
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
tension: 0.4,
|
|
fill: true,
|
|
},
|
|
],
|
|
}
|
|
|
|
const memoryChartData = {
|
|
labels: metrics.map((_, index) => `${index * 5}s ago`).reverse(),
|
|
datasets: [
|
|
{
|
|
label: 'Memory Usage %',
|
|
data: metrics.map(m => m.memory_usage).reverse(),
|
|
borderColor: 'rgb(59, 130, 246)',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4,
|
|
fill: true,
|
|
},
|
|
],
|
|
}
|
|
|
|
const apiRequestsData = {
|
|
labels: metrics.map((_, index) => `${index * 5}s`).reverse(),
|
|
datasets: [
|
|
{
|
|
label: 'API Requests/min',
|
|
data: metrics.map(m => m.api_requests_per_minute).reverse(),
|
|
backgroundColor: 'rgba(34, 197, 94, 0.8)',
|
|
borderColor: 'rgb(34, 197, 94)',
|
|
},
|
|
],
|
|
}
|
|
|
|
const latest = getLatestMetric()
|
|
|
|
return (
|
|
<AdminLayout title="Real-time Monitoring">
|
|
<Head>
|
|
<title>Real-time Monitoring - Hliadka.sk Admin</title>
|
|
</Head>
|
|
|
|
{/* Connection Status */}
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className={`flex items-center space-x-2 px-3 py-1 rounded-full text-sm font-medium ${
|
|
isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
<span>{isConnected ? 'Pripojené' : 'Odpojené'}</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setAutoRefresh(!autoRefresh)}
|
|
className={`flex items-center space-x-2 px-3 py-1 rounded-md text-sm font-medium transition-colors ${
|
|
autoRefresh
|
|
? 'bg-primary-100 text-primary-800 hover:bg-primary-200'
|
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
<ArrowPathIcon className={`w-4 h-4 ${autoRefresh ? 'animate-spin' : ''}`} />
|
|
<span>{autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="text-sm text-gray-500">
|
|
Posledná aktualizácia: {new Date().toLocaleTimeString('sk-SK')}
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Status Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">CPU Usage</p>
|
|
<p className={`text-2xl font-bold ${getStatusColor(latest.cpu_usage || 0, { warning: 70, critical: 90 })}`}>
|
|
{latest.cpu_usage || 0}%
|
|
</p>
|
|
</div>
|
|
<CpuChipIcon className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<div className="mt-2">
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${
|
|
(latest.cpu_usage || 0) >= 90 ? 'bg-red-500' :
|
|
(latest.cpu_usage || 0) >= 70 ? 'bg-yellow-500' : 'bg-green-500'
|
|
}`}
|
|
style={{ width: `${latest.cpu_usage || 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Memory Usage</p>
|
|
<p className={`text-2xl font-bold ${getStatusColor(latest.memory_usage || 0, { warning: 80, critical: 95 })}`}>
|
|
{latest.memory_usage || 0}%
|
|
</p>
|
|
</div>
|
|
<CircleStackIcon className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<div className="mt-2">
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full ${
|
|
(latest.memory_usage || 0) >= 95 ? 'bg-red-500' :
|
|
(latest.memory_usage || 0) >= 80 ? 'bg-yellow-500' : 'bg-green-500'
|
|
}`}
|
|
style={{ width: `${latest.memory_usage || 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Active Connections</p>
|
|
<p className="text-2xl font-bold text-primary-600">
|
|
{latest.active_connections || 0}
|
|
</p>
|
|
</div>
|
|
<GlobeAltIcon className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">Databáza + API</p>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Response Time</p>
|
|
<p className={`text-2xl font-bold ${getStatusColor(latest.response_time || 0, { warning: 500, critical: 1000 })}`}>
|
|
{latest.response_time || 0}ms
|
|
</p>
|
|
</div>
|
|
<ClockIcon className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">Priemerný API response</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Real-time Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
<div className="card p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">CPU Usage - Posledných 100s</h3>
|
|
<Line
|
|
data={cpuChartData}
|
|
options={{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
ticks: {
|
|
callback: (value) => `${value}%`
|
|
}
|
|
},
|
|
x: {
|
|
display: false
|
|
}
|
|
},
|
|
elements: {
|
|
point: {
|
|
radius: 0
|
|
}
|
|
}
|
|
}}
|
|
height={200}
|
|
/>
|
|
</div>
|
|
|
|
<div className="card p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Memory Usage - Posledných 100s</h3>
|
|
<Line
|
|
data={memoryChartData}
|
|
options={{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
ticks: {
|
|
callback: (value) => `${value}%`
|
|
}
|
|
},
|
|
x: {
|
|
display: false
|
|
}
|
|
},
|
|
elements: {
|
|
point: {
|
|
radius: 0
|
|
}
|
|
}
|
|
}}
|
|
height={200}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Activity */}
|
|
<div className="card p-6 mb-8">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">API Activity - Requests per Minute</h3>
|
|
<Bar
|
|
data={apiRequestsData}
|
|
options={{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
},
|
|
x: {
|
|
display: false
|
|
}
|
|
},
|
|
}}
|
|
height={150}
|
|
/>
|
|
</div>
|
|
|
|
{/* Alerts and Active Users */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* System Alerts */}
|
|
<div className="card p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Systémové upozornenia</h3>
|
|
<div className="space-y-3">
|
|
{alerts.length > 0 ? alerts.map((alert) => (
|
|
<div key={alert.id} className={`p-3 rounded-md border-l-4 ${
|
|
alert.type === 'error' ? 'bg-red-50 border-red-400' :
|
|
alert.type === 'warning' ? 'bg-yellow-50 border-yellow-400' :
|
|
'bg-blue-50 border-blue-400'
|
|
}`}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
{alert.type === 'error' ? (
|
|
<ExclamationTriangleIcon className="h-5 w-5 text-red-500" />
|
|
) : alert.type === 'warning' ? (
|
|
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
|
|
) : (
|
|
<CheckCircleIcon className="h-5 w-5 text-blue-500" />
|
|
)}
|
|
<span className="font-medium text-sm">{alert.title}</span>
|
|
</div>
|
|
<span className="text-xs text-gray-500">
|
|
{new Date(alert.timestamp).toLocaleTimeString('sk-SK')}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mt-1">{alert.message}</p>
|
|
</div>
|
|
)) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<CheckCircleIcon className="h-12 w-12 mx-auto mb-2 text-green-500" />
|
|
<p>Žiadne aktívne upozornenia</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Users */}
|
|
<div className="card p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Aktívni používatelia</h3>
|
|
<div className="space-y-3">
|
|
{activeUsers.map((user) => (
|
|
<div key={user.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
|
<div>
|
|
<div className="font-medium text-sm text-gray-900">{user.name}</div>
|
|
<div className="text-xs text-gray-500">{user.email}</div>
|
|
<div className="text-xs text-gray-400">{user.location}</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xs text-gray-500">
|
|
{new Date(user.last_activity).toLocaleTimeString('sk-SK')}
|
|
</div>
|
|
<div className="text-xs text-gray-400">{user.ip_address}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{activeUsers.length === 0 && (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<p>Žiadni aktívni používatelia</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|
|
|
|
export default RealtimeMonitoring |