seo-generator-server/lib/modes/ManualServer.js
Trouve Alexis 870cfb0340 [200~add step-by-step versioning system with Google Sheets integration
- Add intermediate saves (v1.0-v1.4) to Generated_Articles_Versioned
  - Fix compiled_text pipeline (generatedTexts object structure)
  - Add /api/workflow-modulaire endpoint with version tracking
  - Create test-modulaire.html interface with real-time logs
  - Support parent-child linking via Parent_Article_ID
2025-09-06 16:38:20 +08:00

840 lines
28 KiB
JavaScript

// ========================================
// FICHIER: ManualServer.js
// RESPONSABILITÉ: Mode MANUAL - Interface Client + API + WebSocket
// FONCTIONNALITÉS: Dashboard, tests modulaires, API complète
// ========================================
const express = require('express');
const cors = require('cors');
const path = require('path');
const WebSocket = require('ws');
const { logSh } = require('../ErrorReporting');
const { handleModularWorkflow, benchmarkStacks } = require('../main_modulaire');
/**
* SERVEUR MODE MANUAL
* Interface client complète avec API, WebSocket et dashboard
*/
class ManualServer {
constructor(options = {}) {
this.config = {
port: options.port || process.env.MANUAL_PORT || 3000,
wsPort: options.wsPort || process.env.WS_PORT || 8081,
host: options.host || '0.0.0.0',
...options
};
this.app = null;
this.server = null;
this.wsServer = null;
this.activeClients = new Set();
this.stats = {
sessions: 0,
requests: 0,
testsExecuted: 0,
startTime: Date.now(),
lastActivity: null
};
this.isRunning = false;
}
// ========================================
// DÉMARRAGE ET ARRÊT
// ========================================
/**
* Démarre le serveur MANUAL complet
*/
async start() {
if (this.isRunning) {
logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING');
return;
}
logSh('🎯 Démarrage ManualServer...', 'INFO');
try {
// 1. Configuration Express
await this.setupExpressApp();
// 2. Routes API
this.setupAPIRoutes();
// 3. Interface Web
this.setupWebInterface();
// 4. WebSocket pour logs temps réel
await this.setupWebSocketServer();
// 5. Démarrage serveur HTTP
await this.startHTTPServer();
// 6. Monitoring
this.startMonitoring();
this.isRunning = true;
this.stats.startTime = Date.now();
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO');
logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO');
} catch (error) {
logSh(`❌ Erreur démarrage ManualServer: ${error.message}`, 'ERROR');
await this.stop();
throw error;
}
}
/**
* Arrête le serveur MANUAL
*/
async stop() {
if (!this.isRunning) return;
logSh('🛑 Arrêt ManualServer...', 'INFO');
try {
// Déconnecter tous les clients WebSocket
this.disconnectAllClients();
// Arrêter WebSocket server
if (this.wsServer) {
this.wsServer.close();
this.wsServer = null;
}
// Arrêter serveur HTTP
if (this.server) {
await new Promise((resolve) => {
this.server.close(() => resolve());
});
this.server = null;
}
this.isRunning = false;
logSh('✅ ManualServer arrêté', 'INFO');
} catch (error) {
logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING');
}
}
// ========================================
// CONFIGURATION EXPRESS
// ========================================
/**
* Configure l'application Express
*/
async setupExpressApp() {
this.app = express();
// Middleware de base
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cors());
// Middleware de logs des requêtes
this.app.use((req, res, next) => {
this.stats.requests++;
this.stats.lastActivity = Date.now();
logSh(`📥 ${req.method} ${req.path} - ${req.ip}`, 'TRACE');
next();
});
// Fichiers statiques
this.app.use(express.static(path.join(__dirname, '../../public')));
logSh('⚙️ Express configuré', 'DEBUG');
}
/**
* Configure les routes API
*/
setupAPIRoutes() {
// Route de status
this.app.get('/api/status', (req, res) => {
res.json({
success: true,
mode: 'MANUAL',
status: 'running',
uptime: Date.now() - this.stats.startTime,
stats: { ...this.stats },
clients: this.activeClients.size,
timestamp: new Date().toISOString()
});
});
// Test modulaire individuel
this.app.post('/api/test-modulaire', async (req, res) => {
await this.handleTestModulaire(req, res);
});
// 🆕 Workflow modulaire avec sauvegarde par étapes
this.app.post('/api/workflow-modulaire', async (req, res) => {
await this.handleWorkflowModulaire(req, res);
});
// Benchmark modulaire complet
this.app.post('/api/benchmark-modulaire', async (req, res) => {
await this.handleBenchmarkModulaire(req, res);
});
// Configuration modulaire disponible
this.app.get('/api/modulaire-config', (req, res) => {
this.handleModulaireConfig(req, res);
});
// Stats détaillées
this.app.get('/api/stats', (req, res) => {
res.json({
success: true,
stats: {
...this.stats,
uptime: Date.now() - this.stats.startTime,
activeClients: this.activeClients.size,
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
}
});
});
// Gestion d'erreurs API
this.app.use('/api/*', (error, req, res, next) => {
logSh(`❌ Erreur API ${req.path}: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur serveur interne',
message: error.message,
timestamp: new Date().toISOString()
});
});
logSh('🛠️ Routes API configurées', 'DEBUG');
}
// ========================================
// HANDLERS API
// ========================================
/**
* Gère les tests modulaires individuels
*/
async handleTestModulaire(req, res) {
try {
const config = req.body;
this.stats.testsExecuted++;
logSh(`🧪 Test modulaire: ${config.selectiveStack} + ${config.adversarialMode} + ${config.humanSimulationMode} + ${config.patternBreakingMode}`, 'INFO');
// Validation des paramètres
if (!config.rowNumber || config.rowNumber < 2) {
return res.status(400).json({
success: false,
error: 'Numéro de ligne invalide (minimum 2)'
});
}
// Exécution du test
const result = await handleModularWorkflow({
...config,
source: 'manual_server_api'
});
logSh(`✅ Test modulaire terminé: ${result.stats.totalDuration}ms`, 'INFO');
res.json({
success: true,
message: 'Test modulaire terminé avec succès',
stats: result.stats,
config: config,
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur test modulaire: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: error.message,
config: req.body,
timestamp: new Date().toISOString()
});
}
}
/**
* Gère les benchmarks modulaires
*/
async handleBenchmarkModulaire(req, res) {
try {
const { rowNumber = 2 } = req.body;
logSh(`📊 Benchmark modulaire ligne ${rowNumber}...`, 'INFO');
if (rowNumber < 2) {
return res.status(400).json({
success: false,
error: 'Numéro de ligne invalide (minimum 2)'
});
}
const benchResults = await benchmarkStacks(rowNumber);
const successfulTests = benchResults.filter(r => r.success);
const avgDuration = successfulTests.length > 0 ?
successfulTests.reduce((sum, r) => sum + r.duration, 0) / successfulTests.length : 0;
this.stats.testsExecuted += benchResults.length;
logSh(`📊 Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`, 'INFO');
res.json({
success: true,
message: `Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`,
summary: {
totalTests: benchResults.length,
successfulTests: successfulTests.length,
failedTests: benchResults.length - successfulTests.length,
averageDuration: Math.round(avgDuration),
rowNumber: rowNumber
},
results: benchResults,
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur benchmark modulaire: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: error.message,
timestamp: new Date().toISOString()
});
}
}
/**
* 🆕 Gère les workflows modulaires avec sauvegarde par étapes
*/
async handleWorkflowModulaire(req, res) {
try {
const config = req.body;
this.stats.testsExecuted++;
// Configuration par défaut avec sauvegarde activée
const workflowConfig = {
rowNumber: config.rowNumber || 2,
selectiveStack: config.selectiveStack || 'standardEnhancement',
adversarialMode: config.adversarialMode || 'light',
humanSimulationMode: config.humanSimulationMode || 'none',
patternBreakingMode: config.patternBreakingMode || 'none',
saveIntermediateSteps: config.saveIntermediateSteps !== false, // Par défaut true
source: 'api_manual_server'
};
logSh(`🔗 Workflow modulaire avec étapes: ligne ${workflowConfig.rowNumber}`, 'INFO');
logSh(` 📋 Config: ${workflowConfig.selectiveStack} + ${workflowConfig.adversarialMode} + ${workflowConfig.humanSimulationMode} + ${workflowConfig.patternBreakingMode}`, 'DEBUG');
logSh(` 💾 Sauvegarde étapes: ${workflowConfig.saveIntermediateSteps ? 'ACTIVÉE' : 'DÉSACTIVÉE'}`, 'INFO');
// Validation des paramètres
if (workflowConfig.rowNumber < 2) {
return res.status(400).json({
success: false,
error: 'Numéro de ligne invalide (minimum 2)'
});
}
// Exécution du workflow complet
const startTime = Date.now();
const result = await handleModularWorkflow(workflowConfig);
const duration = Date.now() - startTime;
// Statistiques finales
const finalStats = {
duration,
success: result.success,
versionsCreated: result.stats?.versionHistory?.length || 1,
parentArticleId: result.stats?.parentArticleId,
finalArticleId: result.storageResult?.articleId,
totalModifications: {
selective: result.stats?.selectiveEnhancements || 0,
adversarial: result.stats?.adversarialModifications || 0,
human: result.stats?.humanSimulationModifications || 0,
pattern: result.stats?.patternBreakingModifications || 0
},
finalLength: result.stats?.finalLength || 0
};
logSh(`✅ Workflow modulaire terminé: ${finalStats.versionsCreated} versions créées en ${duration}ms`, 'INFO');
res.json({
success: true,
message: `Workflow modulaire terminé avec succès (${finalStats.versionsCreated} versions sauvegardées)`,
config: workflowConfig,
stats: finalStats,
versionHistory: result.stats?.versionHistory,
result: {
parentArticleId: finalStats.parentArticleId,
finalArticleId: finalStats.finalArticleId,
duration: finalStats.duration,
modificationsCount: Object.values(finalStats.totalModifications).reduce((sum, val) => sum + val, 0),
finalWordCount: result.storageResult?.wordCount,
personality: result.stats?.personality
},
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur workflow modulaire: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: error.message,
timestamp: new Date().toISOString()
});
}
}
/**
* Retourne la configuration modulaire
*/
handleModulaireConfig(req, res) {
try {
const config = {
selectiveStacks: [
{ value: 'lightEnhancement', name: 'Light Enhancement', description: 'Améliorations légères' },
{ value: 'standardEnhancement', name: 'Standard Enhancement', description: 'Améliorations standard' },
{ value: 'fullEnhancement', name: 'Full Enhancement', description: 'Améliorations complètes' },
{ value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' },
{ value: 'fluidityFocus', name: 'Fluidity Focus', description: 'Focus fluidité' },
{ value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' }
],
adversarialModes: [
{ value: 'none', name: 'None', description: 'Aucune technique adversariale' },
{ value: 'light', name: 'Light', description: 'Techniques adversariales légères' },
{ value: 'standard', name: 'Standard', description: 'Techniques adversariales standard' },
{ value: 'heavy', name: 'Heavy', description: 'Techniques adversariales intensives' },
{ value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' }
],
humanSimulationModes: [
{ value: 'none', name: 'None', description: 'Aucune simulation humaine' },
{ value: 'lightSimulation', name: 'Light Simulation', description: 'Simulation légère' },
{ value: 'standardSimulation', name: 'Standard Simulation', description: 'Simulation standard' },
{ value: 'heavySimulation', name: 'Heavy Simulation', description: 'Simulation intensive' },
{ value: 'adaptiveSimulation', name: 'Adaptive Simulation', description: 'Simulation adaptative' },
{ value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' },
{ value: 'temporalFocus', name: 'Temporal Focus', description: 'Focus temporel' }
],
patternBreakingModes: [
{ value: 'none', name: 'None', description: 'Aucun pattern breaking' },
{ value: 'lightPatternBreaking', name: 'Light Pattern Breaking', description: 'Pattern breaking léger' },
{ value: 'standardPatternBreaking', name: 'Standard Pattern Breaking', description: 'Pattern breaking standard' },
{ value: 'heavyPatternBreaking', name: 'Heavy Pattern Breaking', description: 'Pattern breaking intensif' },
{ value: 'adaptivePatternBreaking', name: 'Adaptive Pattern Breaking', description: 'Pattern breaking adaptatif' },
{ value: 'syntaxFocus', name: 'Syntax Focus', description: 'Focus syntaxe uniquement' },
{ value: 'connectorsFocus', name: 'Connectors Focus', description: 'Focus connecteurs uniquement' }
]
};
res.json({
success: true,
config: config,
timestamp: new Date().toISOString()
});
} catch (error) {
logSh(`❌ Erreur config modulaire: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: error.message
});
}
}
// ========================================
// INTERFACE WEB
// ========================================
/**
* Configure l'interface web
*/
setupWebInterface() {
// Page d'accueil - Dashboard MANUAL
this.app.get('/', (req, res) => {
res.send(this.generateManualDashboard());
});
// Route 404
this.app.use('*', (req, res) => {
res.status(404).json({
success: false,
error: 'Route non trouvée',
path: req.originalUrl,
mode: 'MANUAL',
message: 'Cette route n\'existe pas en mode MANUAL'
});
});
logSh('🌐 Interface web configurée', 'DEBUG');
}
/**
* Génère le dashboard HTML du mode MANUAL
*/
generateManualDashboard() {
const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000);
return `
<!DOCTYPE html>
<html>
<head>
<title>SEO Generator - Mode MANUAL</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 40px; }
.header h1 { color: #2d3748; margin-bottom: 10px; }
.mode-badge { background: #48bb78; color: white; padding: 8px 16px; border-radius: 20px; font-weight: bold; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }
.stat-card { background: #f7fafc; padding: 20px; border-radius: 10px; text-align: center; border: 1px solid #e2e8f0; }
.stat-number { font-size: 2em; font-weight: bold; color: #2d3748; }
.stat-label { color: #718096; margin-top: 5px; }
.section { margin: 30px 0; padding: 25px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f9f9f9; }
.section h2 { color: #2d3748; margin-bottom: 15px; }
.button { display: inline-block; padding: 12px 24px; margin: 8px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; transition: all 0.2s; }
.button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
.button.success { background: linear-gradient(135deg, #48bb78, #38a169); }
.button.warning { background: linear-gradient(135deg, #ed8936, #dd6b20); }
.alert { padding: 15px; margin: 20px 0; border-radius: 8px; border-left: 4px solid; }
.alert.success { background: #f0fff4; border-color: #48bb78; color: #22543d; }
.alert.info { background: #ebf8ff; border-color: #4299e1; color: #2a4365; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 SEO Generator Server</h1>
<span class="mode-badge">MODE MANUAL</span>
<p style="color: #718096; margin-top: 15px;">Interface Client + API + Tests Modulaires</p>
</div>
<div class="alert success">
<strong>✅ Mode MANUAL Actif</strong><br>
Interface complète disponible • WebSocket temps réel • API complète
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">${uptime}s</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.stats.requests}</div>
<div class="stat-label">Requêtes</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.activeClients.size}</div>
<div class="stat-label">Clients WebSocket</div>
</div>
<div class="stat-card">
<div class="stat-number">${this.stats.testsExecuted}</div>
<div class="stat-label">Tests Exécutés</div>
</div>
</div>
<div class="section">
<h2>🧪 Interface Test Modulaire</h2>
<p>Interface avancée pour tester toutes les combinaisons modulaires avec logs temps réel.</p>
<a href="/test-modulaire.html" target="_blank" class="button">🚀 Ouvrir Interface Test</a>
<a href="/api/modulaire-config" target="_blank" class="button success">📋 Configuration API</a>
</div>
<div class="section">
<h2>📊 Monitoring & API</h2>
<p>Endpoints disponibles en mode MANUAL.</p>
<a href="/api/status" target="_blank" class="button">📊 Status API</a>
<a href="/api/stats" target="_blank" class="button">📈 Statistiques</a>
<button onclick="testConnection()" class="button success">🔍 Test Connexion</button>
</div>
<div class="section">
<h2>🌐 WebSocket Logs</h2>
<p>Logs temps réel sur <strong>ws://localhost:${this.config.wsPort}</strong></p>
<div id="wsStatus" style="margin-top: 10px; padding: 10px; background: #e2e8f0; border-radius: 5px;">
Status: <span id="wsStatusText">Déconnecté</span>
</div>
</div>
<div class="section">
<h2>💡 Informations Mode MANUAL</h2>
<ul style="color: #4a5568; line-height: 1.6;">
<li><strong>Interface Client</strong> : Dashboard complet et interface de test</li>
<li><strong>API Complète</strong> : Tests individuels, benchmarks, configuration</li>
<li><strong>WebSocket</strong> : Logs temps réel sur port ${this.config.wsPort}</li>
<li><strong>Multi-Client</strong> : Plusieurs utilisateurs simultanés</li>
<li><strong>Pas de GSheets</strong> : Données test simulées ou fournies</li>
</ul>
</div>
</div>
<script>
let ws = null;
function testConnection() {
fetch('/api/status')
.then(res => res.json())
.then(data => {
alert('✅ Connexion OK: ' + (data.mode || 'Mode MANUAL actif') + ' - Uptime: ' + Math.floor(data.uptime/1000) + 's');
})
.catch(err => {
alert('❌ Erreur connexion: ' + err.message);
});
}
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:${this.config.wsPort}');
ws.onopen = () => {
document.getElementById('wsStatusText').textContent = 'Connecté ✅';
document.getElementById('wsStatus').style.background = '#c6f6d5';
};
ws.onclose = () => {
document.getElementById('wsStatusText').textContent = 'Déconnecté ❌';
document.getElementById('wsStatus').style.background = '#fed7d7';
setTimeout(connectWebSocket, 3000);
};
} catch (error) {
console.warn('WebSocket non disponible:', error.message);
}
}
// Auto-connect WebSocket
connectWebSocket();
// Refresh stats every 30s
setInterval(() => {
window.location.reload();
}, 30000);
</script>
</body>
</html>
`;
}
// ========================================
// WEBSOCKET SERVER
// ========================================
/**
* Configure le serveur WebSocket pour logs temps réel
*/
async setupWebSocketServer() {
try {
this.wsServer = new WebSocket.Server({
port: this.config.wsPort,
host: this.config.host
});
this.wsServer.on('connection', (ws, req) => {
this.handleWebSocketConnection(ws, req);
});
this.wsServer.on('error', (error) => {
logSh(`❌ Erreur WebSocket: ${error.message}`, 'ERROR');
});
logSh(`📡 WebSocket Server démarré sur ws://${this.config.host}:${this.config.wsPort}`, 'DEBUG');
} catch (error) {
logSh(`⚠️ Impossible de démarrer WebSocket: ${error.message}`, 'WARNING');
// Continue sans WebSocket si erreur
}
}
/**
* Gère les nouvelles connexions WebSocket
*/
handleWebSocketConnection(ws, req) {
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const clientIP = req.socket.remoteAddress;
this.activeClients.add({ id: clientId, ws, ip: clientIP, connectedAt: Date.now() });
this.stats.sessions++;
logSh(`📡 Nouveau client WebSocket: ${clientId} (${clientIP})`, 'DEBUG');
// Message de bienvenue
ws.send(JSON.stringify({
type: 'welcome',
message: 'Connecté aux logs temps réel SEO Generator (Mode MANUAL)',
clientId: clientId,
timestamp: new Date().toISOString()
}));
// Gestion fermeture
ws.on('close', () => {
this.activeClients.delete(clientId);
logSh(`📡 Client WebSocket déconnecté: ${clientId}`, 'DEBUG');
});
// Gestion erreurs
ws.on('error', (error) => {
logSh(`⚠️ Erreur client WebSocket ${clientId}: ${error.message}`, 'WARNING');
});
}
/**
* Diffuse un message à tous les clients WebSocket
*/
broadcastToClients(logData) {
if (this.activeClients.size === 0) return;
const message = JSON.stringify({
type: 'log',
...logData,
timestamp: new Date().toISOString()
});
this.activeClients.forEach(client => {
if (client.ws.readyState === WebSocket.OPEN) {
try {
client.ws.send(message);
} catch (error) {
// Client déconnecté, le supprimer
this.activeClients.delete(client.id);
}
}
});
}
/**
* Déconnecte tous les clients WebSocket
*/
disconnectAllClients() {
this.activeClients.forEach(client => {
try {
client.ws.close();
} catch (error) {
// Ignore les erreurs de fermeture
}
});
this.activeClients.clear();
logSh('📡 Tous les clients WebSocket déconnectés', 'DEBUG');
}
// ========================================
// SERVEUR HTTP
// ========================================
/**
* Démarre le serveur HTTP
*/
async startHTTPServer() {
return new Promise((resolve, reject) => {
try {
this.server = this.app.listen(this.config.port, this.config.host, () => {
resolve();
});
this.server.on('error', (error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
}
// ========================================
// MONITORING
// ========================================
/**
* Démarre le monitoring du serveur
*/
startMonitoring() {
const MONITOR_INTERVAL = 30000; // 30 secondes
this.monitorInterval = setInterval(() => {
this.performMonitoring();
}, MONITOR_INTERVAL);
logSh('💓 Monitoring ManualServer démarré', 'DEBUG');
}
/**
* Effectue le monitoring périodique
*/
performMonitoring() {
const memUsage = process.memoryUsage();
const uptime = Date.now() - this.stats.startTime;
logSh(`💓 ManualServer Health - Clients: ${this.activeClients.size} | Requêtes: ${this.stats.requests} | RAM: ${Math.round(memUsage.rss / 1024 / 1024)}MB`, 'TRACE');
// Nettoyage clients WebSocket morts
this.cleanupDeadClients();
}
/**
* Nettoie les clients WebSocket déconnectés
*/
cleanupDeadClients() {
let cleaned = 0;
this.activeClients.forEach(client => {
if (client.ws.readyState !== WebSocket.OPEN) {
this.activeClients.delete(client.id);
cleaned++;
}
});
if (cleaned > 0) {
logSh(`🧹 ${cleaned} clients WebSocket morts nettoyés`, 'DEBUG');
}
}
// ========================================
// ÉTAT ET CONTRÔLES
// ========================================
/**
* Vérifie s'il y a des clients actifs
*/
hasActiveClients() {
return this.activeClients.size > 0;
}
/**
* Retourne l'état du serveur MANUAL
*/
getStatus() {
return {
isRunning: this.isRunning,
config: { ...this.config },
stats: {
...this.stats,
uptime: Date.now() - this.stats.startTime
},
activeClients: this.activeClients.size,
urls: {
dashboard: `http://localhost:${this.config.port}`,
testInterface: `http://localhost:${this.config.port}/test-modulaire.html`,
apiStatus: `http://localhost:${this.config.port}/api/status`,
websocket: `ws://localhost:${this.config.wsPort}`
}
};
}
}
// ============= EXPORTS =============
module.exports = { ManualServer };