Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
/**
* Service intelligent pour monitoring serveur via Server-Sent Events
* Avec fallback polling et reconnexion automatique
*/
import { API_CONFIG } from '../config/api';
export interface ServerStatusEvent {
status: 'UP' | 'DOWN' | 'MAINTENANCE';
timestamp: string;
message: string;
system?: {
version: string;
uptimeSeconds: number;
activeConnections: number;
};
}
export interface ServerStatusListener {
(isOnline: boolean, event?: ServerStatusEvent): void;
}
export class ServerStatusService {
private eventSource: EventSource | null = null;
private listeners: ServerStatusListener[] = [];
private isOnline: boolean = true;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectDelay: number = 1000;
private fallbackPolling: boolean = false;
private fallbackInterval: NodeJS.Timeout | null = null;
private lastEventTime: number = Date.now();
private heartbeatTimeout: NodeJS.Timeout | null = null;
constructor() {
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
// Vérifier si on est côté client (évite les erreurs SSR)
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
}
/**
* Démarrer le monitoring serveur
*/
public start(): void {
console.log('🚀 Démarrage monitoring serveur SSE');
this.initSSE();
}
/**
* Arrêter le monitoring
*/
public stop(): void {
console.log('🛑 Arrêt monitoring serveur');
this.cleanup();
// Vérifier si on est côté client avant de supprimer l'event listener
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}
}
/**
* S'abonner aux changements de statut
*/
public onStatusChange(listener: ServerStatusListener): () => void {
this.listeners.push(listener);
// Retourner une fonction de désabonnement
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
/**
* Obtenir le statut actuel
*/
public getCurrentStatus(): boolean {
return this.isOnline;
}
/**
* Initialiser Server-Sent Events
*/
private initSSE(): void {
try {
// Retirer /api/v1 de l'URL de base pour l'endpoint SSE
const baseUrl = API_CONFIG.baseURL.replace('/api/v1', '');
const sseUrl = `${baseUrl}/api/server-status/stream`;
console.log('📡 Connexion SSE:', sseUrl);
this.eventSource = new EventSource(sseUrl);
this.eventSource.onopen = (event) => {
console.log('✅ SSE connecté avec succès');
this.reconnectAttempts = 0;
this.fallbackPolling = false;
this.stopFallbackPolling();
this.updateStatus(true);
this.resetHeartbeatTimeout();
};
this.eventSource.onmessage = (event) => {
try {
const statusEvent: ServerStatusEvent = JSON.parse(event.data);
console.log('📊 SSE reçu:', statusEvent);
this.lastEventTime = Date.now();
this.updateStatus(statusEvent.status === 'UP', statusEvent);
this.resetHeartbeatTimeout();
} catch (error) {
console.error('❌ Erreur parsing SSE:', error);
}
};
this.eventSource.onerror = (event) => {
console.warn('⚠️ Erreur SSE, tentative de reconnexion...');
this.handleSSEError();
};
} catch (error) {
console.error('❌ Impossible d\'initialiser SSE:', error);
this.startFallbackPolling();
}
}
/**
* Gérer les erreurs SSE et reconnexion
*/
private handleSSEError(): void {
this.updateStatus(false);
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Backoff exponentiel
console.log(`🔄 Reconnexion SSE dans ${delay}ms (tentative ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.cleanup();
this.initSSE();
}, delay);
} else {
console.warn('❌ Échec reconnexion SSE, passage en mode polling');
this.startFallbackPolling();
}
}
/**
* Démarrer le polling de secours
*/
private startFallbackPolling(): void {
if (this.fallbackPolling) return;
console.log('🔄 Démarrage polling de secours');
this.fallbackPolling = true;
const poll = async () => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
// Retirer /api/v1 pour l'endpoint de fallback aussi
const baseUrl = API_CONFIG.baseURL.replace('/api/v1', '');
const response = await fetch(`${baseUrl}/api/server-status/current`, {
signal: controller.signal,
headers: { 'Accept': 'application/json' }
});
clearTimeout(timeoutId);
if (response.ok) {
const statusEvent: ServerStatusEvent = await response.json();
this.updateStatus(statusEvent.status === 'UP', statusEvent);
// Essayer de rétablir SSE si le serveur est de nouveau UP
if (statusEvent.status === 'UP' && this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('🔄 Serveur récupéré, tentative de rétablissement SSE');
this.stopFallbackPolling();
this.reconnectAttempts = 0;
setTimeout(() => this.initSSE(), 2000);
}
} else {
this.updateStatus(false);
}
} catch (error) {
console.warn('⚠️ Erreur polling de secours:', error);
this.updateStatus(false);
}
};
// Poll immédiat puis toutes les 10 secondes
poll();
this.fallbackInterval = setInterval(poll, 10000);
}
/**
* Arrêter le polling de secours
*/
private stopFallbackPolling(): void {
if (this.fallbackInterval) {
clearInterval(this.fallbackInterval);
this.fallbackInterval = null;
}
this.fallbackPolling = false;
}
/**
* Timeout de heartbeat pour détecter les connexions perdues
*/
private resetHeartbeatTimeout(): void {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
}
// Si pas de message pendant 45 secondes (heartbeat = 30s), considérer comme déconnecté
this.heartbeatTimeout = setTimeout(() => {
console.warn('💔 Pas de heartbeat SSE, connexion probablement perdue');
this.handleSSEError();
}, 45000);
}
/**
* Gérer la visibilité de l'onglet
*/
private handleVisibilityChange(): void {
// Vérifier si on est côté client
if (typeof document === 'undefined') return;
if (!document.hidden) {
console.log('👁️ Onglet redevenu visible, vérification connexion SSE');
// Vérifier si la connexion SSE est toujours active
if (this.eventSource && this.eventSource.readyState === EventSource.CLOSED) {
console.log('🔄 SSE fermé, reconnexion...');
this.reconnectAttempts = 0;
this.initSSE();
}
}
}
/**
* Mettre à jour le statut et notifier les listeners
*/
private updateStatus(isOnline: boolean, event?: ServerStatusEvent): void {
const wasOnline = this.isOnline;
this.isOnline = isOnline;
if (wasOnline !== isOnline) {
console.log(`🔄 Statut serveur changé: ${isOnline ? 'ON' : 'OFF'}LINE`);
}
// Notifier tous les listeners
this.listeners.forEach(listener => {
try {
listener(isOnline, event);
} catch (error) {
console.error('❌ Erreur dans listener status:', error);
}
});
}
/**
* Nettoyer les ressources
*/
private cleanup(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.stopFallbackPolling();
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = null;
}
}
}
// Instance singleton avec initialisation paresseuse
let _serverStatusService: ServerStatusService | null = null;
export const getServerStatusService = (): ServerStatusService => {
if (!_serverStatusService && typeof window !== 'undefined') {
_serverStatusService = new ServerStatusService();
}
return _serverStatusService!;
};
// Pour la compatibilité avec l'ancien code
export const serverStatusService = typeof window !== 'undefined' ? getServerStatusService() : null;