Files
service-finder/frontend/admin/components/SystemHealthTile.vue
2026-03-23 21:43:40 +00:00

591 lines
21 KiB
Vue

<template>
<v-card
color="blue-grey-darken-1"
variant="tonal"
class="h-100 d-flex flex-column"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-server" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">System Health</span>
</div>
<div class="d-flex align-center">
<v-chip size="small" :color="overallStatusColor" class="mr-2">
<v-icon :icon="overallStatusIcon" size="small" class="mr-1"></v-icon>
{{ overallStatusText }}
</v-chip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
size="x-small"
variant="text"
v-bind="props"
class="text-caption"
>
{{ selectedEnvironment }}
<v-icon icon="mdi-chevron-down" size="small"></v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="env in environmentOptions"
:key="env.value"
@click="selectedEnvironment = env.value"
>
<v-list-item-title>{{ env.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-card-title>
<v-card-text class="flex-grow-1 pa-0">
<div class="pa-4">
<!-- System Status Overview -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">System Status</div>
<v-row dense>
<v-col v-for="component in systemComponents" :key="component.name" cols="6" sm="3">
<v-card variant="outlined" class="pa-2">
<div class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center">
<v-icon :icon="component.icon" size="small" :color="component.statusColor" class="mr-2"></v-icon>
<span class="text-caption font-weight-medium">{{ component.name }}</span>
</div>
<div class="text-caption text-grey mt-1">{{ component.description }}</div>
</div>
<div class="text-right">
<div class="text-caption" :class="`text-${component.statusColor}`">{{ component.status }}</div>
<div class="text-caption text-grey">{{ component.uptime }}%</div>
</div>
</div>
<!-- Response Time Indicator -->
<div v-if="component.responseTime" class="mt-2">
<div class="d-flex justify-space-between">
<span class="text-caption text-grey">Response</span>
<span class="text-caption" :class="getResponseTimeColor(component.responseTime)">{{ component.responseTime }}ms</span>
</div>
<v-progress-linear
:model-value="Math.min(component.responseTime / 10, 100)"
height="4"
:color="getResponseTimeColor(component.responseTime)"
class="mt-1"
></v-progress-linear>
</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- API Response Times Chart -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">API Response Times (Last 24h)</div>
<div class="chart-container" style="height: 150px;">
<canvas ref="responseTimeChart"></canvas>
</div>
</div>
<!-- Database Metrics -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Database Metrics</div>
<v-row dense>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.connections > 80 ? 'text-error' : 'text-success'">{{ databaseMetrics.connections }}</div>
<div class="text-caption text-grey">Connections</div>
<div class="text-caption">{{ databaseMetrics.activeConnections }} active</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.queryTime > 500 ? 'text-error' : databaseMetrics.queryTime > 200 ? 'text-warning' : 'text-success'">{{ databaseMetrics.queryTime }}ms</div>
<div class="text-caption text-grey">Avg Query Time</div>
<div class="text-caption">{{ databaseMetrics.queriesPerSecond }} qps</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.cacheHitRate < 80 ? 'text-error' : databaseMetrics.cacheHitRate < 90 ? 'text-warning' : 'text-success'">{{ databaseMetrics.cacheHitRate }}%</div>
<div class="text-caption text-grey">Cache Hit Rate</div>
<div class="text-caption">{{ formatBytes(databaseMetrics.cacheSize) }} cache</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.replicationLag > 1000 ? 'text-error' : databaseMetrics.replicationLag > 500 ? 'text-warning' : 'text-success'">{{ databaseMetrics.replicationLag }}ms</div>
<div class="text-caption text-grey">Replication Lag</div>
<div class="text-caption">{{ databaseMetrics.replicationStatus }}</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- Server Resources -->
<div>
<div class="text-subtitle-2 font-weight-medium mb-2">Server Resources</div>
<v-row dense>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">CPU Usage</span>
<span class="text-caption" :class="serverResources.cpu > 80 ? 'text-error' : serverResources.cpu > 60 ? 'text-warning' : 'text-success'">{{ serverResources.cpu }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.cpu"
height="8"
:color="serverResources.cpu > 80 ? 'error' : serverResources.cpu > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ serverResources.cpuCores }} cores @ {{ serverResources.cpuFrequency }}GHz</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Memory Usage</span>
<span class="text-caption" :class="serverResources.memory > 80 ? 'text-error' : serverResources.memory > 60 ? 'text-warning' : 'text-success'">{{ serverResources.memory }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.memory"
height="8"
:color="serverResources.memory > 80 ? 'error' : serverResources.memory > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.memoryUsed) }} / {{ formatBytes(serverResources.memoryTotal) }}</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Disk I/O</span>
<span class="text-caption" :class="serverResources.diskIO > 80 ? 'text-error' : serverResources.diskIO > 60 ? 'text-warning' : 'text-success'">{{ serverResources.diskIO }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.diskIO"
height="8"
:color="serverResources.diskIO > 80 ? 'error' : serverResources.diskIO > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.diskRead) }}/s read, {{ formatBytes(serverResources.diskWrite) }}/s write</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Network</span>
<span class="text-caption" :class="serverResources.network > 80 ? 'text-error' : serverResources.network > 60 ? 'text-warning' : 'text-success'">{{ serverResources.network }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.network"
height="8"
:color="serverResources.network > 80 ? 'error' : serverResources.network > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.networkIn) }}/s in, {{ formatBytes(serverResources.networkOut) }}/s out</div>
</v-card>
</v-col>
</v-row>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-3">
<div class="d-flex justify-space-between align-center w-100">
<div class="text-caption">
<v-icon icon="mdi-clock" size="small" class="mr-1"></v-icon>
Last check: {{ lastCheckTime }}
<v-chip size="x-small" color="green" class="ml-2">
Auto-refresh: {{ refreshInterval }}s
</v-chip>
</div>
<div class="d-flex">
<v-btn
size="x-small"
variant="text"
@click="refreshHealth"
:loading="isRefreshing"
>
<v-icon icon="mdi-refresh" size="small" class="mr-1"></v-icon>
Refresh
</v-btn>
<v-btn
size="x-small"
variant="text"
@click="toggleAutoRefresh"
:color="autoRefresh ? 'primary' : 'grey'"
class="ml-2"
>
<v-icon :icon="autoRefresh ? 'mdi-pause' : 'mdi-play'" size="small" class="mr-1"></v-icon>
{{ autoRefresh ? 'Pause' : 'Resume' }}
</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { Chart, registerables } from 'chart.js'
// Register Chart.js components
Chart.register(...registerables)
// Types
interface SystemComponent {
name: string
description: string
status: 'healthy' | 'degraded' | 'down'
statusColor: string
icon: string
uptime: number
responseTime?: number
}
interface DatabaseMetrics {
connections: number
activeConnections: number
queryTime: number
queriesPerSecond: number
cacheHitRate: number
cacheSize: number
replicationLag: number
replicationStatus: string
}
interface ServerResources {
cpu: number
cpuCores: number
cpuFrequency: number
memory: number
memoryUsed: number
memoryTotal: number
diskIO: number
diskRead: number
diskWrite: number
network: number
networkIn: number
networkOut: number
}
// State
const selectedEnvironment = ref('production')
const isRefreshing = ref(false)
const autoRefresh = ref(true)
const refreshInterval = ref(30)
const responseTimeChart = ref<HTMLCanvasElement | null>(null)
let chartInstance: Chart | null = null
let refreshTimer: number | null = null
// Environment options
const environmentOptions = [
{ label: 'Production', value: 'production' },
{ label: 'Staging', value: 'staging' },
{ label: 'Development', value: 'development' },
{ label: 'Testing', value: 'testing' }
]
// System components data
const systemComponents = ref<SystemComponent[]>([
{ name: 'API Gateway', description: 'Main API endpoint', status: 'healthy', statusColor: 'success', icon: 'mdi-api', uptime: 99.9, responseTime: 45 },
{ name: 'Database', description: 'PostgreSQL cluster', status: 'healthy', statusColor: 'success', icon: 'mdi-database', uptime: 99.95, responseTime: 120 },
{ name: 'Cache', description: 'Redis cache layer', status: 'healthy', statusColor: 'success', icon: 'mdi-memory', uptime: 99.8, responseTime: 8 },
{ name: 'Message Queue', description: 'RabbitMQ broker', status: 'degraded', statusColor: 'warning', icon: 'mdi-message-processing', uptime: 98.5, responseTime: 250 },
{ name: 'File Storage', description: 'S3-compatible storage', status: 'healthy', statusColor: 'success', icon: 'mdi-file-cloud', uptime: 99.7, responseTime: 180 },
{ name: 'Authentication', description: 'OAuth2/JWT service', status: 'healthy', statusColor: 'success', icon: 'mdi-shield-account', uptime: 99.9, responseTime: 65 },
{ name: 'Monitoring', description: 'Prometheus/Grafana', status: 'healthy', statusColor: 'success', icon: 'mdi-chart-line', uptime: 99.8, responseTime: 95 },
{ name: 'Load Balancer', description: 'Nginx reverse proxy', status: 'healthy', statusColor: 'success', icon: 'mdi-load-balancer', uptime: 99.99, responseTime: 12 }
])
const databaseMetrics = ref<DatabaseMetrics>({
connections: 64,
activeConnections: 42,
queryTime: 85,
queriesPerSecond: 1250,
cacheHitRate: 92,
cacheSize: 2147483648, // 2GB
replicationLag: 45,
replicationStatus: 'Synced'
})
const serverResources = ref<ServerResources>({
cpu: 42,
cpuCores: 8,
cpuFrequency: 3.2,
memory: 68,
memoryUsed: 1090519040, // ~1GB
memoryTotal: 17179869184, // 16GB
diskIO: 28,
diskRead: 5242880, // 5MB/s
diskWrite: 1048576, // 1MB/s
network: 45,
networkIn: 2097152, // 2MB/s
networkOut: 1048576 // 1MB/s
})
// Computed properties
const overallStatus = computed(() => {
const healthyCount = systemComponents.value.filter(c => c.status === 'healthy').length
const totalCount = systemComponents.value.length
if (healthyCount === totalCount) return 'healthy'
if (healthyCount >= totalCount * 0.8) return 'degraded'
return 'critical'
})
const overallStatusColor = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'green'
case 'degraded': return 'orange'
case 'critical': return 'red'
default: return 'grey'
}
})
const overallStatusIcon = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'mdi-check-circle'
case 'degraded': return 'mdi-alert-circle'
case 'critical': return 'mdi-close-circle'
default: return 'mdi-help-circle'
}
})
const overallStatusText = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'All Systems Normal'
case 'degraded': return 'Minor Issues'
case case 'critical': return 'Critical Issues'
default: return 'Unknown'
}
})
const lastCheckTime = computed(() => {
const now = new Date()
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
})
// Helper functions
const getResponseTimeColor = (responseTime: number) => {
if (responseTime < 100) return 'success'
if (responseTime < 300) return 'warning'
return 'error'
}
const formatBytes = (bytes: number) => {
if (bytes >= 1073741824) {
return `${(bytes / 1073741824).toFixed(1)} GB`
} else if (bytes >= 1048576) {
return `${(bytes / 1048576).toFixed(1)} MB`
} else if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(1)} KB`
}
return `${bytes} B`
}
// Chart functions
const initChart = () => {
if (!responseTimeChart.value) return
// Destroy existing chart
if (chartInstance) {
chartInstance.destroy()
}
const ctx = responseTimeChart.value.getContext('2d')
if (!ctx) return
// Generate mock response time data for last 24 hours
const labels = Array.from({ length: 24 }, (_, i) => {
const hour = new Date(Date.now() - (23 - i) * 3600000)
return hour.getHours().toString().padStart(2, '0') + ':00'
})
const data = labels.map(() => {
const base = 50
const spike = Math.random() > 0.9 ? 300 : 0
const variance = Math.random() * 40
return Math.round(base + variance + spike)
})
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'API Response Time (ms)',
data,
borderColor: '#2196F3',
backgroundColor: 'rgba(33, 150, 243, 0.1)',
tension: 0.4,
fill: true,
pointBackgroundColor: (context) => {
const value = context.dataset.data[context.dataIndex] as number
return value > 200 ? '#F44336' : value > 100 ? '#FF9800' : '#4CAF50'
},
pointBorderColor: '#FFFFFF',
pointBorderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (context) => {
return `Response Time: ${context.raw}ms`
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
maxRotation: 0,
callback: (value, index) => {
// Show only every 3rd hour label
return index % 3 === 0 ? labels[index] : ''
}
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Milliseconds (ms)'
},
ticks: {
callback: (value) => `${value}ms`
}
}
}
}
})
}
// Auto-refresh management
const startAutoRefresh = () => {
if (refreshTimer) clearInterval(refreshTimer)
refreshTimer = setInterval(() => {
refreshHealth()
}, refreshInterval.value * 1000) as unknown as number
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
// Actions
const refreshHealth = () => {
if (isRefreshing.value) return
isRefreshing.value = true
// Simulate API call
setTimeout(() => {
// Update system components with random variations
systemComponents.value.forEach(component => {
// Random status changes (rare)
if (Math.random() > 0.95) {
component.status = Math.random() > 0.7 ? 'degraded' : 'healthy'
component.statusColor = component.status === 'healthy' ? 'success' : 'warning'
}
// Update response times
if (component.responseTime) {
const variation = Math.random() * 40 - 20
component.responseTime = Math.max(10, Math.round(component.responseTime + variation))
}
// Update uptime (slight variations)
component.uptime = Math.min(99.99, component.uptime + (Math.random() * 0.1 - 0.05))
})
// Update database metrics
databaseMetrics.value.connections = Math.round(64 + Math.random() * 20 - 10)
databaseMetrics.value.activeConnections = Math.round(databaseMetrics.value.connections * 0.7)
databaseMetrics.value.queryTime = Math.round(85 + Math.random() * 30 - 15)
databaseMetrics.value.queriesPerSecond = Math.round(1250 + Math.random() * 200 - 100)
databaseMetrics.value.cacheHitRate = Math.min(99, Math.round(92 + Math.random() * 4 - 2))
databaseMetrics.value.replicationLag = Math.round(45 + Math.random() * 20 - 10)
// Update server resources
serverResources.value.cpu = Math.round(42 + Math.random() * 20 - 10)
serverResources.value.memory = Math.round(68 + Math.random() * 10 - 5)
serverResources.value.diskIO = Math.round(28 + Math.random() * 15 - 7)
serverResources.value.network = Math.round(45 + Math.random() * 20 - 10)
// Update chart
initChart()
isRefreshing.value = false
}, 800)
}
// Lifecycle hooks
onMounted(() => {
nextTick(() => {
initChart()
})
// Start auto-refresh if enabled
if (autoRefresh.value) {
startAutoRefresh()
}
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.destroy()
}
stopAutoRefresh()
})
</script>
<style scoped>
.chart-container {
position: relative;
width: 100%;
}
.h-100 {
height: 100%;
}
.v-progress-linear {
border-radius: 4px;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>