Class_generator/Legacy/export_logger/websocket-server.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

402 lines
14 KiB
JavaScript

#!/usr/bin/env node
// Serveur WebSocket simple pour recevoir les logs temps réel
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const pino = require('pino');
const http = require('http');
const url = require('url');
const crypto = require('crypto');
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
const wsPort = 8082;
const httpPort = 8083;
// Configuration du logger Pino avec fichier daté
const now = new Date();
const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `class-generator-${timestamp}.log`);
// Créer le dossier logs s'il n'existe pas
const logsDir = path.join(__dirname, '..', 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
console.log('📁 Dossier logs créé');
}
// Logger Pino avec sortie fichier et console
const logger = pino(
{
level: 'debug',
timestamp: pino.stdTimeFunctions.isoTime,
},
pino.multistream([
{ stream: pino.destination({ dest: logFile, mkdir: true, sync: false }) },
{ stream: pino.transport({ target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss.l' } }) }
])
);
// Configuration DigitalOcean
const DO_CONFIG = {
ACCESS_KEY: 'DO8018LC8QF7CFBF7E2K',
SECRET_KEY: 'RLH4bUidH4zb1XQAtBUeUnA4vjizdkQ78D1fOZ5gYpk',
REGION: 'fra1',
ENDPOINT: 'https://autocollant.fra1.digitaloceanspaces.com',
BUCKET_ENDPOINT: 'https://fra1.digitaloceanspaces.com',
BUCKET: 'autocollant',
CONTENT_PATH: 'Class_generator/ContentMe'
};
// Fonctions AWS Signature
function sha256(message) {
return crypto.createHash('sha256').update(message, 'utf8').digest('hex');
}
function hmacSha256(key, message) {
return crypto.createHmac('sha256', key).update(message, 'utf8').digest();
}
async function generateAWSSignature(method, targetUrl) {
const now = new Date();
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
const timeStamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '') + 'Z';
const urlObj = new URL(targetUrl);
const host = urlObj.hostname;
const canonicalUri = urlObj.pathname;
const canonicalQueryString = urlObj.search ? urlObj.search.slice(1) : '';
const canonicalHeaders = `host:${host}\nx-amz-date:${timeStamp}\n`;
const signedHeaders = 'host;x-amz-date';
const payloadHash = method === 'GET' ? sha256('') : 'UNSIGNED-PAYLOAD';
const canonicalRequest = [
method,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
payloadHash
].join('\n');
const algorithm = 'AWS4-HMAC-SHA256';
const credentialScope = `${dateStamp}/${DO_CONFIG.REGION}/s3/aws4_request`;
const canonicalRequestHash = sha256(canonicalRequest);
const stringToSign = [
algorithm,
timeStamp,
credentialScope,
canonicalRequestHash
].join('\n');
const kDate = hmacSha256('AWS4' + DO_CONFIG.SECRET_KEY, dateStamp);
const kRegion = hmacSha256(kDate, DO_CONFIG.REGION);
const kService = hmacSha256(kRegion, 's3');
const kSigning = hmacSha256(kService, 'aws4_request');
const signature = hmacSha256(kSigning, stringToSign).toString('hex');
const authorization = `${algorithm} Credential=${DO_CONFIG.ACCESS_KEY}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return {
'Authorization': authorization,
'X-Amz-Date': timeStamp,
'X-Amz-Content-Sha256': payloadHash
};
}
// Serveur HTTP proxy pour DigitalOcean
const httpServer = http.createServer(async (req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.method === 'GET' && req.url === '/do-proxy/_list') {
// Route spéciale pour lister tous les fichiers dans le dossier
logger.info(`📂 DÉBUT - Listing du dossier ContentMe`);
try {
// Utiliser l'endpoint du bucket directement
const listUrl = `${DO_CONFIG.BUCKET_ENDPOINT}/${DO_CONFIG.BUCKET}?list-type=2&prefix=${DO_CONFIG.CONTENT_PATH}/`;
logger.info(`🌐 List URL: ${listUrl}`);
const headers = await generateAWSSignature('GET', listUrl);
logger.info(`📋 Headers générés pour listing: ${JSON.stringify(headers, null, 2)}`);
const response = await fetch(listUrl, {
method: 'GET',
headers: headers
});
if (response.ok) {
const xmlText = await response.text();
logger.info(`✅ Listing reçu: ${xmlText.length} caractères`);
// Parser le XML pour extraire les noms de fichiers
const fileList = parseS3ListResponse(xmlText);
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify({ files: fileList }));
logger.info(`📁 ${fileList.length} fichiers listés`);
} else {
const errorText = await response.text();
logger.error(`❌ Erreur listing: ${response.status} - ${errorText}`);
// Si c'est un 403, essayer une approche différente
if (response.status === 403) {
logger.info(`🔄 Tentative avec URL alternative...`);
// Pour l'instant on retourne l'erreur
res.writeHead(403, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
res.end(JSON.stringify({
error: 'ListBucket permission denied',
message: 'Les clés actuelles ne permettent pas le listing. Utilisation de la liste connue.',
knownFiles: ['english-class-demo.json', 'sbs-level-7-8-new.json']
}));
} else {
res.writeHead(500, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' });
res.end('Error listing files');
}
}
} catch (error) {
logger.error(`💥 Exception listing: ${error.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' });
res.end(`Error: ${error.message}`);
}
return;
}
if ((req.method === 'GET' || req.method === 'HEAD') && req.url.startsWith('/do-proxy/')) {
const filename = req.url.replace('/do-proxy/', '');
const targetUrl = `${DO_CONFIG.ENDPOINT}/${DO_CONFIG.CONTENT_PATH}/${filename}`;
logger.info(`🔗 DÉBUT - Proxy ${req.method} request: ${filename}`);
logger.info(`🌐 Target URL: ${targetUrl}`);
logger.info(`🔑 ACCESS_KEY: ${DO_CONFIG.ACCESS_KEY}`);
logger.info(`🔐 SECRET_KEY: ${DO_CONFIG.SECRET_KEY.substring(0, 8)}...`);
try {
logger.info(`🔧 Génération de la signature AWS...`);
const headers = await generateAWSSignature(req.method, targetUrl);
logger.info(`📋 Headers générés: ${JSON.stringify(headers, null, 2)}`);
logger.info(`📤 Envoi de la requête à DigitalOcean...`);
const fetchStart = Date.now();
const response = await fetch(targetUrl, {
method: req.method,
headers: headers
});
const fetchDuration = Date.now() - fetchStart;
logger.info(`⏱️ Durée requête DO: ${fetchDuration}ms`);
logger.info(`📡 Status DO: ${response.status} ${response.statusText}`);
logger.info(`📝 Headers réponse DO: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
if (response.ok) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
if (req.method === 'GET') {
const content = await response.text();
res.end(content);
logger.info(`✅ SUCCÈS - Proxy GET: ${filename} (${content.length} chars)`);
} else {
// HEAD request - no body
res.end();
logger.info(`✅ SUCCÈS - Proxy HEAD: ${filename} (headers only)`);
}
} else {
const errorText = await response.text();
logger.error(`❌ ERREUR DO - Status: ${response.status}, Body: ${errorText}`);
res.writeHead(response.status, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*'
});
res.end(errorText);
}
} catch (error) {
logger.error(`💥 EXCEPTION PROXY - Type: ${error.constructor.name}`);
logger.error(`💥 Message: ${error.message}`);
logger.error(`💥 Stack: ${error.stack}`);
res.writeHead(500, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*'
});
res.end(`Error: ${error.message}`);
}
return;
}
// Route par défaut
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found\nUse: /do-proxy/filename.json');
});
httpServer.listen(httpPort, '0.0.0.0', () => {
logger.info(`🌐 Serveur HTTP proxy démarré sur le port ${httpPort} (toutes interfaces)`);
logger.info(`🔗 URL proxy: http://localhost:${httpPort}/do-proxy/filename.json`);
logger.info(`🌍 Accessible depuis Windows: http://localhost:${httpPort}/do-proxy/filename.json`);
});
// Créer le serveur WebSocket sur toutes les interfaces
const wss = new WebSocket.Server({ port: wsPort, host: '0.0.0.0' });
logger.info(`🚀 Serveur WebSocket démarré sur le port ${wsPort}`);
logger.info(`📡 En attente de connexions...`);
logger.info(`📝 Logs enregistrés dans: ${logFile}`);
// Garder trace des clients connectés
const clients = new Set();
wss.on('connection', function connection(ws) {
logger.info('✅ Nouveau client connecté');
clients.add(ws);
// Envoyer un message de bienvenue
const welcomeMessage = {
timestamp: new Date().toISOString(),
level: 'INFO',
message: '🎉 Connexion WebSocket établie - Logs en temps réel actifs'
};
ws.send(JSON.stringify(welcomeMessage));
// Gérer les messages du client (si nécessaire)
ws.on('message', function incoming(data) {
try {
const message = JSON.parse(data);
// Enregistrer le log dans le fichier via Pino
const level = (message.level || 'INFO').toLowerCase();
switch (level) {
case 'error':
logger.error(message.message);
break;
case 'warn':
case 'warning':
logger.warn(message.message);
break;
case 'debug':
logger.debug(message.message);
break;
case 'trace':
logger.trace(message.message);
break;
default:
logger.info(message.message);
}
// DIFFUSER LE LOG À TOUS LES CLIENTS CONNECTÉS !
broadcastLog(message);
} catch (error) {
logger.warn('📨 Message reçu (brut):', data.toString());
}
});
// Nettoyer quand le client se déconnecte
ws.on('close', function close() {
logger.info('❌ Client déconnecté');
clients.delete(ws);
});
// Gérer les erreurs
ws.on('error', function error(err) {
logger.error('❌ Erreur WebSocket:', err.message);
clients.delete(ws);
});
});
// Fonction pour diffuser un log à tous les clients connectés
function broadcastLog(logData) {
const message = JSON.stringify(logData);
let sentCount = 0;
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
try {
ws.send(message);
sentCount++;
} catch (error) {
logger.error('❌ Erreur envoi vers client:', error.message);
clients.delete(ws);
}
} else {
// Nettoyer les connexions fermées
clients.delete(ws);
}
});
if (sentCount > 0) {
logger.debug(`📡 Log diffusé à ${sentCount} client(s): [${logData.level}] ${logData.message.substring(0, 50)}${logData.message.length > 50 ? '...' : ''}`);
}
}
// Parser la réponse XML S3 pour extraire les noms de fichiers
function parseS3ListResponse(xmlText) {
const files = [];
// Simple regex pour extraire les clés (noms de fichiers) du XML
const keyRegex = /<Key>([^<]+)<\/Key>/g;
let match;
while ((match = keyRegex.exec(xmlText)) !== null) {
const fullPath = match[1];
// Extraire juste le nom du fichier (après le dernier /)
const filename = fullPath.split('/').pop();
// Filtrer les fichiers vides et les dossiers
if (filename && filename.length > 0 && !filename.endsWith('/')) {
files.push(filename);
}
}
return files;
}
// Export pour utilisation dans d'autres modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { broadcastLog, wss, clients };
}
// Gérer l'arrêt propre
process.on('SIGINT', () => {
logger.info('\n🛑 Arrêt du serveur WebSocket...');
// Fermer toutes les connexions
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
// Fermer le serveur
wss.close(() => {
logger.info('✅ Serveur WebSocket arrêté');
logger.flush();
process.exit(0);
});
});
// Message de test toutes les 30 secondes pour vérifier que ça marche
setInterval(() => {
if (clients.size > 0) {
const testMessage = {
timestamp: new Date().toISOString(),
level: 'DEBUG',
message: `💓 Heartbeat - ${clients.size} client(s) connecté(s)`
};
broadcastLog(testMessage);
}
}, 30000);