veza/apps/web/src/components/monitoring/MonitoringDashboard.tsx
senke 1e97e4305c monitoring: implement Monitor 7 - create monitoring dashboard
- Created MonitoringDashboard component to visualize system metrics
- Added monitoring tab to AdminDashboardPage (/admin route)
- Displays validation metrics (total, successful, failed, failure rate)
- Shows top 5 endpoints with most validation failures
- Displays Sentry error tracking status and configuration
- Shows performance monitoring information
- Auto-refreshes metrics every 30 seconds
- Manual refresh button available
- Links to Sentry dashboard when configured
- Complete monitoring solution for system health visibility
2026-01-16 14:27:05 +01:00

423 lines
15 KiB
TypeScript

/**
* Monitor 7: Monitoring Dashboard Component
* Visualizes validation metrics, API performance, and error tracking
*/
import { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Activity,
AlertTriangle,
CheckCircle,
XCircle,
TrendingUp,
Clock,
ExternalLink,
RefreshCw,
} from 'lucide-react';
import { validationMetrics } from '@/services/api/client';
import { env } from '@/config/env';
import { logger } from '@/utils/logger';
interface MonitoringMetrics {
validation: {
total: number;
successful: number;
failed: number;
failureRate: number;
lastFailureTime?: string;
lastSuccessTime?: string;
failuresByEndpoint: Record<string, number>;
};
sentry: {
enabled: boolean;
dsnConfigured: boolean;
};
}
export function MonitoringDashboard() {
const [metrics, setMetrics] = useState<MonitoringMetrics | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const loadMetrics = () => {
try {
const validationMetricsData = validationMetrics.getMetrics();
setMetrics({
validation: {
total: validationMetricsData.totalValidations,
successful: validationMetricsData.successfulValidations,
failed: validationMetricsData.failedValidations,
failureRate: validationMetricsData.failureRate,
lastFailureTime: validationMetricsData.lastFailureTime,
lastSuccessTime: validationMetricsData.lastSuccessTime,
failuresByEndpoint: validationMetricsData.failuresByEndpoint,
},
sentry: {
enabled: !!env.SENTRY_DSN && import.meta.env.PROD,
dsnConfigured: !!env.SENTRY_DSN,
},
});
setLastUpdate(new Date());
} catch (error) {
logger.error('[MonitoringDashboard] Failed to load metrics', {
error: error instanceof Error ? error.message : String(error),
});
}
};
useEffect(() => {
loadMetrics();
// Refresh metrics every 30 seconds
const interval = setInterval(loadMetrics, 30000);
return () => clearInterval(interval);
}, []);
if (!metrics) {
return (
<div className="flex items-center justify-center h-[400px]">
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Chargement des métriques...</p>
</div>
</div>
);
}
const getFailureRateColor = (rate: number): string => {
if (rate === 0) return 'text-green-600';
if (rate < 1) return 'text-yellow-600';
if (rate < 5) return 'text-orange-600';
return 'text-red-600';
};
const getFailureRateBadge = (rate: number) => {
if (rate === 0) return 'success';
if (rate < 1) return 'default';
if (rate < 5) return 'warning';
return 'error';
};
const topFailureEndpoints = Object.entries(metrics.validation.failuresByEndpoint)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Activity className="h-6 w-6" />
Monitoring du Système
</h2>
<p className="text-muted-foreground text-sm mt-1">
Métriques de validation, performance API et suivi des erreurs
</p>
</div>
<div className="flex items-center gap-2">
<p className="text-xs text-muted-foreground">
Dernière mise à jour: {lastUpdate.toLocaleTimeString('fr-FR')}
</p>
<Button
variant="outline"
size="sm"
onClick={loadMetrics}
className="gap-2"
>
<RefreshCw className="h-4 w-4" />
Actualiser
</Button>
</div>
</div>
{/* Validation Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Activity className="h-4 w-4" />
Total Validations
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{metrics.validation.total.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground mt-1">
Validations depuis le démarrage
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-600" />
Validations Réussies
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{metrics.validation.successful.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground mt-1">
{metrics.validation.total > 0
? (
(metrics.validation.successful / metrics.validation.total) *
100
).toFixed(1)
: 0}
% de réussite
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-600" />
Validations Échouées
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{metrics.validation.failed.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground mt-1">
{metrics.validation.failureRate.toFixed(2)}% d'échec
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Taux d'Échec
</CardTitle>
</CardHeader>
<CardContent>
<div
className={`text-2xl font-bold ${getFailureRateColor(
metrics.validation.failureRate,
)}`}
>
{metrics.validation.failureRate.toFixed(2)}%
</div>
<Badge
variant={getFailureRateBadge(metrics.validation.failureRate)}
className="mt-2"
>
{metrics.validation.failureRate === 0
? 'Parfait'
: metrics.validation.failureRate < 1
? 'Bon'
: metrics.validation.failureRate < 5
? 'Attention'
: 'Critique'}
</Badge>
</CardContent>
</Card>
</div>
{/* Detailed Metrics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Validation Details */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Détails des Validations
</CardTitle>
<CardDescription>
Statistiques détaillées des validations API
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
Dernière validation réussie:
</span>
<span className="text-sm font-medium">
{metrics.validation.lastSuccessTime
? new Date(
metrics.validation.lastSuccessTime,
).toLocaleString('fr-FR')
: 'Aucune'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
Dernière validation échouée:
</span>
<span className="text-sm font-medium text-red-600">
{metrics.validation.lastFailureTime
? new Date(
metrics.validation.lastFailureTime,
).toLocaleString('fr-FR')
: 'Aucune'}
</span>
</div>
</div>
{topFailureEndpoints.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium mb-2">
Endpoints avec le plus d'échecs:
</h4>
<div className="space-y-1">
{topFailureEndpoints.map(([endpoint, count]) => (
<div
key={endpoint}
className="flex justify-between items-center p-2 bg-muted rounded text-sm"
>
<code className="text-xs">{endpoint}</code>
<Badge variant="error">{count}</Badge>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Error Tracking Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Suivi des Erreurs
</CardTitle>
<CardDescription>
Configuration et statut du suivi des erreurs
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-muted rounded">
<div className="flex items-center gap-2">
{metrics.sentry.enabled ? (
<CheckCircle className="h-5 w-5 text-green-600" />
) : (
<XCircle className="h-5 w-5 text-muted-foreground" />
)}
<span className="text-sm font-medium">Sentry Error Tracking</span>
</div>
<Badge
variant={metrics.sentry.enabled ? 'success' : 'secondary'}
>
{metrics.sentry.enabled ? 'Actif' : 'Inactif'}
</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-muted rounded">
<div className="flex items-center gap-2">
{metrics.sentry.dsnConfigured ? (
<CheckCircle className="h-5 w-5 text-green-600" />
) : (
<XCircle className="h-5 w-5 text-muted-foreground" />
)}
<span className="text-sm font-medium">DSN Configuré</span>
</div>
<Badge
variant={metrics.sentry.dsnConfigured ? 'success' : 'secondary'}
>
{metrics.sentry.dsnConfigured ? 'Oui' : 'Non'}
</Badge>
</div>
</div>
{metrics.sentry.enabled && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-950 rounded border border-blue-200 dark:border-blue-800">
<p className="text-xs text-blue-900 dark:text-blue-100 mb-2">
Les erreurs sont automatiquement envoyées à Sentry pour le suivi
et l'analyse.
</p>
<Button
variant="outline"
size="sm"
className="w-full gap-2"
onClick={() => {
// Open Sentry dashboard (if URL is known)
// This would typically be configured in env vars
const sentryDsn = env.SENTRY_DSN;
const sentryUrl = sentryDsn
? `https://sentry.io/organizations/${sentryDsn.split('@')[1]?.split('/')[0]}/issues/`
: 'https://sentry.io';
window.open(sentryUrl, '_blank');
}}
>
<ExternalLink className="h-4 w-4" />
Ouvrir Sentry Dashboard
</Button>
</div>
)}
{!metrics.sentry.enabled && (
<div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-950 rounded border border-yellow-200 dark:border-yellow-800">
<p className="text-xs text-yellow-900 dark:text-yellow-100">
Le suivi des erreurs Sentry n'est pas activé. Configurez{' '}
<code className="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">
VITE_SENTRY_DSN
</code>{' '}
pour activer le suivi des erreurs en production.
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Performance Monitoring Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Performance Monitoring
</CardTitle>
<CardDescription>
Les métriques de performance sont suivies automatiquement
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-3 bg-muted rounded">
<div className="text-sm font-medium mb-1">
Temps de Rendu
</div>
<p className="text-xs text-muted-foreground">
Suivi via Sentry Browser Tracing
</p>
</div>
<div className="p-3 bg-muted rounded">
<div className="text-sm font-medium mb-1">
Temps de Réponse API
</div>
<p className="text-xs text-muted-foreground">
Suivi dans le client API avec détection des requêtes lentes
</p>
</div>
<div className="p-3 bg-muted rounded">
<div className="text-sm font-medium mb-1">
Erreurs Réseau
</div>
<p className="text-xs text-muted-foreground">
Détection des pannes partielles vs complètes
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}