From 1f8688c4aa97fa18017dac2f24eaff11179fd52b Mon Sep 17 00:00:00 2001 From: StillHammer Date: Mon, 15 Sep 2025 23:05:14 +0800 Subject: [PATCH] Fix WebSocket logging system and add comprehensive network features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix WebSocket server to properly broadcast logs to all connected clients - Integrate professional logging system with real-time WebSocket interface - Add network status indicator with DigitalOcean Spaces connectivity - Implement AWS Signature V4 authentication for private bucket access - Add JSON content loader with backward compatibility to JS modules - Restore navigation breadcrumb system with comprehensive logging - Add multiple content formats: JSON + JS with automatic discovery - Enhance top bar with logger toggle and network status indicator - Remove deprecated temp-games module and clean up unused files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 70 ++- CLAUDE_local.md | 269 --------- config/games-config.json | 16 +- css/main.css | 168 +++++- export_logger/EXPORT_INFO.md | 161 +++++ export_logger/ErrorReporting.js | 547 +++++++++++++++++ export_logger/README.md | 310 ++++++++++ export_logger/demo.js | 203 +++++++ export_logger/log-server.cjs | 179 ++++++ export_logger/logs-viewer.html | 928 +++++++++++++++++++++++++++++ export_logger/logviewer.cjs | 338 +++++++++++ export_logger/package-lock.json | 441 ++++++++++++++ export_logger/package.json | 42 ++ export_logger/trace-wrap.js | 9 + export_logger/trace.js | 156 +++++ export_logger/websocket-server.js | 111 ++++ index.html | 164 ++++- js/content/basic-chinese.js | 18 +- js/content/english-class-demo.js | 287 --------- js/content/english-class-demo.json | 244 ++++++++ js/content/sbs-level-7-8-new.json | 213 +++++++ js/content/test-animals.js | 102 ++++ js/core/browser-logger.js | 355 +++++++++++ js/core/content-engine.js | 18 +- js/core/content-factory.js | 16 +- js/core/content-generators.js | 10 +- js/core/content-parsers.js | 12 +- js/core/content-scanner.js | 372 +++++++++++- js/core/env-config.js | 269 +++++++++ js/core/game-loader.js | 17 +- js/core/json-content-loader.js | 391 ++++++++++++ js/core/navigation.js | 68 ++- js/core/simple-logger.js | 117 ++++ js/core/test-logger.js | 27 + js/core/utils.js | 6 +- js/core/websocket-logger.js | 99 +++ js/games/adventure-reader.js | 6 +- js/games/chinese-study.js | 2 +- js/games/fill-the-blank.js | 26 +- js/games/memory-match.js | 26 +- js/games/quiz-game.js | 26 +- js/games/story-builder.js | 2 +- js/games/temp-games.js | 736 ----------------------- js/games/text-reader.js | 26 +- js/games/whack-a-mole-hard.js | 38 +- js/games/whack-a-mole.js | 38 +- js/tools/content-creator.js | 4 +- migrate-console.js | 114 ++++ test-connection.html | 44 ++ test-logs-interface.html | 78 +++ test-websocket-simple.html | 75 +++ 51 files changed, 6473 insertions(+), 1521 deletions(-) delete mode 100644 CLAUDE_local.md create mode 100644 export_logger/EXPORT_INFO.md create mode 100644 export_logger/ErrorReporting.js create mode 100644 export_logger/README.md create mode 100644 export_logger/demo.js create mode 100644 export_logger/log-server.cjs create mode 100644 export_logger/logs-viewer.html create mode 100644 export_logger/logviewer.cjs create mode 100644 export_logger/package-lock.json create mode 100644 export_logger/package.json create mode 100644 export_logger/trace-wrap.js create mode 100644 export_logger/trace.js create mode 100644 export_logger/websocket-server.js delete mode 100644 js/content/english-class-demo.js create mode 100644 js/content/english-class-demo.json create mode 100644 js/content/sbs-level-7-8-new.json create mode 100644 js/content/test-animals.js create mode 100644 js/core/browser-logger.js create mode 100644 js/core/env-config.js create mode 100644 js/core/json-content-loader.js create mode 100644 js/core/simple-logger.js create mode 100644 js/core/test-logger.js create mode 100644 js/core/websocket-logger.js delete mode 100644 js/games/temp-games.js create mode 100644 migrate-console.js create mode 100644 test-connection.html create mode 100644 test-logs-interface.html create mode 100644 test-websocket-simple.html diff --git a/CLAUDE.md b/CLAUDE.md index 5fc2bd7..89135ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,9 @@ Interactive English learning platform for children (8-9 years old) built as a mo - Games and content are loaded dynamically via `GameLoader.loadGame(gameType, contentType)` - All modules register themselves on global objects: `window.GameModules` and `window.ContentModules` - Content is discovered automatically by `ContentScanner` scanning `js/content/` directory +- **JSON Content Support**: New JSON-first architecture with backward compatibility to JS modules +- **JSON Content Loader**: `js/core/json-content-loader.js` transforms JSON content to legacy game format +- **Offline-First Loading**: Content loads from local files first, with DigitalOcean Spaces fallback - Games follow consistent constructor pattern: `new GameClass({ container, content, onScoreUpdate, onGameEnd })` ### URL-Based Navigation @@ -26,6 +29,8 @@ Interactive English learning platform for children (8-9 years old) built as a mo - Browser back/forward supported through `popstate` events - Navigation history maintained in `AppNavigation.navigationHistory` - Breadcrumb navigation with clickable path elements +- **Top Bar**: Fixed header with app title and permanent network status indicator +- **Network Status**: Real-time connectivity indicator (🟢 Online / 🟠 Connecting / 🔴 Offline) - Keyboard shortcuts (ESC = go back) ## Content Module Format @@ -46,7 +51,7 @@ window.ContentModules.ModuleName = { vocabulary: { "word_or_character": { translation: "English translation", - pinyin: "pronunciation guide", // Optional: Chinese pinyin + prononciation: "pronunciation guide", // Optional: pronunciation guide type: "noun|verb|adjective|greeting|number", // Word classification pronunciation: "audio/word.mp3", // Optional: audio file difficulty: "HSK1|HSK2|...", // Optional: individual word difficulty @@ -64,7 +69,7 @@ window.ContentModules.ModuleName = { title: "Grammar Rule Title", explanation: "Detailed explanation", examples: [ - { chinese: "中文例子", english: "English example", pinyin: "zhōng wén lì zi" } + { chinese: "中文例子", english: "English example", prononciation: "zhōng wén lì zi" } ], exercises: [/* grammar-specific exercises */] } @@ -164,12 +169,47 @@ window.ContentModules.ModuleName = { ], // Standard content (backward compatibility) - sentences: [{ english: "...", chinese: "...", pinyin: "..." }], + sentences: [{ english: "...", chinese: "...", prononciation: "..." }], texts: [{ title: "...", content: "...", translation: "..." }], dialogues: [{ conversation: [...] }] }; ``` +### JSON Content Format (New Architecture) + +The platform now supports JSON content files for easier editing and maintenance: + +```json +{ + "name": "Content Name", + "description": "Content description", + "difficulty": "easy|medium|hard", + "vocabulary": { + "word": { + "translation": "French translation", + "prononciation": "pronunciation guide", + "type": "noun|verb|adjective" + } + }, + "sentences": [ + { + "english": "English sentence", + "chinese": "Chinese translation", + "prononciation": "pronunciation" + } + ], + "grammar": { /* grammar rules */ }, + "audio": { /* audio content */ }, + "exercises": { /* exercise definitions */ } +} +``` + +**JSON Content Loader Features:** +- Automatic transformation from JSON to legacy game format +- Backward compatibility with existing JavaScript content modules +- Support for all rich content features (vocabulary, grammar, audio, exercises) +- Offline-first loading with cloud fallback + ### Content Adaptivity System The platform automatically adapts available games and exercises based on content richness: @@ -199,10 +239,10 @@ The platform automatically adapts available games and exercises based on content { vocabulary: { "hello": "你好" } } → Enable: Basic matching, simple quiz → Disable: Audio practice, grammar exercises -→ Suggest: "Add pinyin and audio for pronunciation practice" +→ Suggest: "Add pronunciation guide and audio for pronunciation practice" // Rich multimedia content -{ vocabulary: { "hello": { translation: "你好", pinyin: "nǐ hǎo", pronunciation: "audio/hello.mp3" } } } +{ vocabulary: { "hello": { translation: "你好", prononciation: "nǐ hǎo", pronunciation: "audio/hello.mp3" } } } → Enable: All vocabulary games, audio practice, pronunciation scoring → Unlock: Advanced difficulty levels, speed challenges ``` @@ -230,9 +270,12 @@ window.GameModules.GameName = GameName; ## Configuration System -- Main config: `config/games-config.json` - defines available games and content +- **Main config**: `config/games-config.json` - defines available games and content +- **Environment config**: `js/core/env-config.js` - DigitalOcean Spaces configuration and offline settings +- **Content discovery**: Automatic scanning of both `.js` and `.json` content files - Games can be enabled/disabled via `games.{gameType}.enabled` -- Content modules auto-detected but can be configured in `content` section +- **Cloud Integration**: DigitalOcean Spaces endpoint configuration for remote content +- **Offline-First Strategy**: Local content prioritized, remote fallback with timeout protection - UI settings, scoring rules, and feature flags also in main config ## Development Workflow @@ -246,6 +289,12 @@ Open `index.html` in a web browser - no build process required. All modules load 3. Update `AppNavigation.getDefaultConfig()` if needed ### Adding New Content +**Option 1: JSON Format (Recommended)** +1. Create `js/content/{content-name}.json` with proper JSON structure +2. Content will be auto-discovered and loaded via JSON Content Loader +3. Easier to edit and maintain than JavaScript files + +**Option 2: JavaScript Format (Legacy)** 1. Create `js/content/{content-name}.js` with proper module export 2. Add filename to `ContentScanner.contentFiles` array 3. Content will be auto-discovered on next app load @@ -266,6 +315,8 @@ Open `index.html` in a web browser - no build process required. All modules load - `js/core/content-engine.js` - Content processing engine (484 lines) - `js/core/content-factory.js` - Exercise generation (553 lines) - `js/core/content-parsers.js` - Content parsing utilities (484 lines) +- `js/core/json-content-loader.js` - JSON to legacy format transformation +- `js/core/env-config.js` - Environment and cloud configuration **Game Implementations:** - `js/games/whack-a-mole.js` - Standard version (623 lines) @@ -327,4 +378,7 @@ Open `index.html` in a web browser - no build process required. All modules load - Mobile/tablet adaptation - Touch-friendly interface - Portrait/landscape orientation support -- Fluid layouts that work on various screen sizes \ No newline at end of file +- Fluid layouts that work on various screen sizes +- **Fixed Top Bar**: App title and network status always visible +- **Network Status**: Automatic hiding of status text on mobile devices +- **Content Margin**: Proper spacing to accommodate fixed header \ No newline at end of file diff --git a/CLAUDE_local.md b/CLAUDE_local.md deleted file mode 100644 index c7231d3..0000000 --- a/CLAUDE_local.md +++ /dev/null @@ -1,269 +0,0 @@ -# Architecture Cours d'Anglais Modulaire -*Système de leçons interactives HTML pour enfants de 8-9 ans* - -## 🎯 Objectif -Créer une plateforme modulaire permettant de générer facilement des cours d'anglais interactifs avec différents types de jeux et contenus pédagogiques. - -## 🏗️ Architecture en 3 Niveaux - -### Niveau 1 : Page d'Accueil (`index.html`) -- **Titre** : "Cours d'Anglais Interactif" -- **Options** : - - 🎮 "Créer une leçon personnalisée" → Niveau 2 - - 📊 "Statistiques" (futur) - - ⚙️ "Paramètres" (futur) - -### Niveau 2 : Sélection Type de Jeu (`game-selector.html`) -Interface avec cards cliquables pour chaque type de jeu : -- 🔨 **Whack-a-Mole** (Tape-taupe vocabulaire) -- 🧠 **Memory Game** (Jeu de mémoire) -- ❓ **Quiz Game** (Questions/Réponses) -- 🎯 **Target Game** (Viser les bonnes réponses) -- 📝 **Temp Games** (Module temporaire pour nouveaux jeux) - -### Niveau 3 : Sélection Contenu/Niveau (`level-selector.html`) -Interface dynamique qui s'adapte au jeu choisi : -- 📚 **SBS Level 8** (votre contenu actuel) -- 🐱 **Animals Vocabulary** -- 🌈 **Colors & Numbers** -- 👨‍👩‍👧‍👦 **Family Members** -- 🍎 **Food & Drinks** -- 🏠 **House & Furniture** - -### Niveau 4 : Jeu (`game.html`) -Page générique qui charge dynamiquement : -- Le moteur de jeu sélectionné -- Le contenu pédagogique choisi -- Interface de retour/navigation - -## 📁 Structure des Fichiers - -``` -cours-anglais/ -├── index.html # Accueil -├── game-selector.html # Sélection type de jeu -├── level-selector.html # Sélection niveau/contenu -├── game.html # Page de jeu générique -│ -├── css/ -│ ├── main.css # Styles généraux -│ ├── navigation.css # Styles navigation -│ └── games.css # Styles jeux -│ -├── js/ -│ ├── core/ -│ │ ├── navigation.js # Gestion navigation 3 niveaux -│ │ ├── game-loader.js # Chargement dynamique des jeux -│ │ └── utils.js # Fonctions utilitaires -│ │ -│ ├── games/ -│ │ ├── whack-a-mole.js # Module Whack-a-Mole -│ │ ├── memory-game.js # Module Memory Game -│ │ ├── quiz-game.js # Module Quiz -│ │ ├── target-game.js # Module Target Game -│ │ └── temp-games.js # Module temporaire -│ │ -│ └── content/ -│ ├── sbs-level-8.js # Contenu SBS Level 8 -│ ├── animals.js # Vocabulaire animaux -│ ├── colors.js # Couleurs et nombres -│ ├── family.js # Famille -│ ├── food.js # Nourriture -│ └── house.js # Maison -│ -├── assets/ -│ ├── images/ -│ ├── sounds/ -│ └── icons/ -│ -└── config/ - └── games-config.json # Configuration des jeux disponibles -``` - -## 🔧 Système de Modules - -### Format des Modules de Jeu -```javascript -// Exemple: whack-a-mole.js -class WhackAMoleGame { - constructor(content, container) { - this.content = content; - this.container = container; - this.score = 0; - this.init(); - } - - init() { /* Initialisation */ } - start() { /* Démarrage */ } - stop() { /* Arrêt */ } - destroy() { /* Nettoyage */ } -} - -// Export pour utilisation -window.GameModules = window.GameModules || {}; -window.GameModules.WhackAMole = WhackAMoleGame; -``` - -### Format des Modules de Contenu -```javascript -// Exemple: sbs-level-8.js -window.ContentModules = window.ContentModules || {}; -window.ContentModules.SBSLevel8 = { - name: "SBS Level 8", - description: "Vocabulaire du manuel SBS Level 8", - vocabulary: [ - { english: "cat", french: "chat", image: "cat.jpg" }, - { english: "dog", french: "chien", image: "dog.jpg" }, - // ... - ], - grammar: [ - // Règles de grammaire si nécessaire - ], - difficulty: "intermediate" -}; -``` - -## 🎮 Spécifications des Jeux - -### Whack-a-Mole (Priorité 1) -- **Principe** : Taupes apparaissent avec mots anglais, élève doit taper les bonnes réponses -- **Variantes** : - - Mot anglais affiché, taper la traduction française - - Image affichée, taper le mot anglais - - Son joué, taper le mot correspondant -- **Paramètres** : Vitesse, nombre de taupes, durée - -### Memory Game (Priorité 2) -- **Principe** : Retourner des cartes pour faire des paires mot/traduction ou mot/image -- **Variantes** : Tailles de grille (4x4, 6x6, 8x8) - -### Quiz Game (Priorité 3) -- **Principe** : Questions à choix multiples ou réponses libres -- **Types** : QCM, Vrai/Faux, Compléter la phrase - -### Target Game (Futur) -- **Principe** : Viser avec la souris les bonnes réponses qui bougent -- **Variantes** : Ballons, bulles, cibles - -## 🛠️ Fonctionnalités Transversales - -### Navigation -- Boutons "Retour" sur chaque niveau -- Breadcrumb navigation -- URLs avec paramètres pour partage/bookmark -- Raccourcis clavier (ESC = retour) - -### Système de Scoring -- Points par bonne réponse -- Malus par erreur -- Bonus vitesse -- Historique des scores -- Badges/achievements - -### Accessibilité -- Support clavier complet -- Textes alternatifs pour images -- Contrastes élevés -- Tailles de police ajustables - -### Responsive Design -- Adaptation tablette/mobile -- Interface tactile -- Orientation portrait/paysage - -## 📊 Configuration et Personnalisation - -### Fichier de Configuration Globale -```json -{ - "games": { - "whack-a-mole": { - "enabled": true, - "name": "Whack-a-Mole", - "icon": "🔨", - "description": "Tape sur les bonnes réponses !" - }, - "memory-game": { - "enabled": true, - "name": "Memory Game", - "icon": "🧠", - "description": "Trouve les paires !" - } - }, - "content": { - "sbs-level-8": { - "enabled": true, - "name": "SBS Level 8", - "icon": "📚", - "description": "Vocabulaire manuel SBS" - } - }, - "settings": { - "defaultDifficulty": "easy", - "soundEnabled": true, - "animationsEnabled": true - } -} -``` - -## 🚀 Plan de Développement - -### Phase 1 (Urgent - Demain) -- [x] Architecture de base -- [ ] Navigation 3 niveaux -- [ ] Module Whack-a-Mole -- [ ] Contenu SBS Level 8 -- [ ] Module temporaire avec 2-3 mini-jeux - -### Phase 2 (Semaine suivante) -- [ ] Module Memory Game -- [ ] Contenu Animals -- [ ] Système de scoring -- [ ] Améliorations UI/UX - -### Phase 3 (Semaines suivantes) -- [ ] Module Quiz Game -- [ ] Contenus Colors, Family, Food -- [ ] Système de statistiques -- [ ] Export/Import de contenus - -### Phase 4 (Long terme) -- [ ] Module Target Game -- [ ] Éditeur de contenu intégré -- [ ] Système multi-utilisateurs -- [ ] Progression pédagogique - -## 💡 Notes d'Implémentation - -### URL Routing -- `index.html` → Accueil -- `index.html?page=games` → Sélection jeux -- `index.html?page=levels&game=whack-a-mole` → Sélection niveau -- `index.html?page=play&game=whack-a-mole&content=sbs-level-8` → Jeu - -### Stockage Local -- Scores et progression : `localStorage` -- Préférences utilisateur : `localStorage` -- Cache des contenus : `sessionStorage` - -### Performance -- Lazy loading des modules -- Préchargement des assets critiques -- Compression des images -- Minification JS/CSS en production - -## 🎨 Direction Artistique - -### Palette de Couleurs -- **Primaire** : Bleu (#3B82F6) - Confiance, apprentissage -- **Secondaire** : Vert (#10B981) - Succès, validation -- **Accent** : Orange (#F59E0B) - Énergie, attention -- **Erreur** : Rouge (#EF4444) - Erreurs claires -- **Neutre** : Gris (#6B7280) - Textes, backgrounds - -### Style Visuel -- Design moderne et épuré -- Animations fluides et non-agressives -- Icons emoji + Font Awesome -- Typography lisible (16px minimum) -- Boutons larges et tactiles (44px minimum) \ No newline at end of file diff --git a/config/games-config.json b/config/games-config.json index 1b33ea0..6a01df0 100644 --- a/config/games-config.json +++ b/config/games-config.json @@ -30,16 +30,6 @@ "maxAge": 15, "estimatedTime": 10 }, - "temp-games": { - "enabled": true, - "name": "Mini-Jeux", - "icon": "🎯", - "description": "Jeux temporaires en développement", - "difficulty": "easy", - "minAge": 8, - "maxAge": 12, - "estimatedTime": 3 - }, "story-builder": { "enabled": true, "name": "Story Builder", @@ -125,11 +115,11 @@ "vocabulary_count": 12, "topics": ["vocabulary", "sentences", "dialogues", "sequences"] }, - "sbs-level-7-8": { + "sbs-level-7-8-new": { "enabled": true, "name": "SBS Level 7-8", "icon": "🌍", - "description": "Around the World - Homes, Clothing & Cultures", + "description": "Around the World - Homes, Clothing & Cultures (JSON Format)", "difficulty": "intermediate", "vocabulary_count": 85, "topics": ["homes", "clothing", "neighborhoods", "grammar", "culture"] @@ -138,7 +128,7 @@ "enabled": true, "name": "Basic Chinese", "icon": "🇨🇳", - "description": "Essential Chinese characters, pinyin and vocabulary", + "description": "Essential Chinese characters, pronunciation and vocabulary", "difficulty": "beginner", "vocabulary_count": 25, "topics": ["greetings", "numbers", "family", "basic_characters"] diff --git a/css/main.css b/css/main.css index 7c3a306..90b77ac 100644 --- a/css/main.css +++ b/css/main.css @@ -41,7 +41,7 @@ body { .container { max-width: 1400px; margin: 0 auto; - padding: 20px; + padding: 70px 20px 20px 20px; /* Top padding pour la top bar */ min-height: 100vh; position: relative; } @@ -50,10 +50,150 @@ body { @media (min-width: 1440px) { .container { max-width: 1600px; - padding: 20px 40px; + padding: 70px 40px 20px 40px; } } +/* === TOP BAR === */ +.top-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 50px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + z-index: 1000; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; +} + +.logger-toggle { + background: var(--primary-color); + border: none; + color: white; + padding: 6px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; + margin-left: 10px; + box-shadow: var(--shadow); +} + +.logger-toggle:hover { + background: var(--accent-color); + transform: translateY(-1px); +} + +.top-bar-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); +} + +/* === NETWORK STATUS INDICATOR === */ +.network-status { + display: flex; + align-items: center; + gap: 8px; +} + +.network-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + position: relative; + transition: all 0.3s ease; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); +} + +.network-indicator.connecting { + background: #F59E0B; + animation: pulse-orange 1.5s infinite; +} + +.network-indicator.online { + background: #22C55E; + animation: pulse-green 2s infinite; +} + +.network-indicator.offline { + background: #EF4444; + animation: pulse-red 1s infinite; +} + +.network-status-text { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); +} + +.network-indicator.connecting + .network-status-text { + color: #F59E0B; +} + +.network-indicator.online + .network-status-text { + color: #22C55E; +} + +.network-indicator.offline + .network-status-text { + color: #EF4444; +} + +@keyframes pulse-green { + 0%, 100% { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); + } + 50% { + box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.3); + } +} + +@keyframes pulse-orange { + 0%, 100% { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); + } + 50% { + box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.4); + } +} + +@keyframes pulse-red { + 0%, 100% { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); + } + 50% { + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.4); + } +} + +/* === OLD CONNECTION STATUS INDICATOR - DEPRECATED === */ +.connection-status { + display: none; +} + +.status-indicator { + display: none; +} + +.status-icon { + display: none; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + /* === PAGES === */ .page { display: none; @@ -361,26 +501,38 @@ button { /* === RESPONSIVE === */ @media (max-width: 768px) { .container { - padding: 15px; + padding: 70px 15px 15px 15px; } - + + .top-bar { + padding: 0 15px; + } + + .top-bar-title { + font-size: 1rem; + } + + .network-status-text { + display: none; /* Masquer le texte sur mobile */ + } + .hero h1 { font-size: 2.2rem; } - + .hero p { font-size: 1.1rem; } - + .page-header h2 { font-size: 2rem; } - + .cards-grid { grid-template-columns: 1fr; gap: 20px; } - + .page { padding: 20px; } diff --git a/export_logger/EXPORT_INFO.md b/export_logger/EXPORT_INFO.md new file mode 100644 index 0000000..8c3f34f --- /dev/null +++ b/export_logger/EXPORT_INFO.md @@ -0,0 +1,161 @@ +# 📦 Export Système de Logging SEO Generator + +## 🎯 Contenu de l'export + +Ce dossier contient le système de logging complet extrait du SEO Generator, **sans les dépendances Google Sheets**. + +### 📁 Fichiers inclus + +``` +export_logger/ +├── ErrorReporting.js # 🏠 Système de logging centralisé (nettoyé) +├── trace.js # 🌲 Système de traçage hiérarchique +├── trace-wrap.js # 🔧 Utilitaires de wrapping +├── logviewer.cjs # 📊 Outil CLI de consultation logs +├── logs-viewer.html # 🌐 Interface web temps réel +├── log-server.cjs # 🚀 Serveur WebSocket pour logs +├── package.json # 📦 Configuration npm +├── demo.js # 🎬 Démonstration complète +├── README.md # 📚 Documentation complète +└── EXPORT_INFO.md # 📋 Ce fichier +``` + +## 🧹 Modifications apportées + +### ❌ Supprimé de ErrorReporting.js: +- Toutes les fonctions Google Sheets (`logToGoogleSheets`, `cleanGoogleSheetsLogs`, etc.) +- Configuration `SHEET_ID` et authentification Google +- Imports `googleapis` +- Variables `sheets` et `auth` +- Appels Google Sheets dans `logSh()` et `cleanLogSheet()` + +### ✅ Conservé: +- Système de logging Pino (console + fichier + WebSocket) +- Traçage hiérarchique complet +- Interface web temps réel +- Outils CLI de consultation +- Formatage coloré et timestamps +- Gestion des niveaux (TRACE, DEBUG, INFO, WARN, ERROR) + +## 🚀 Intégration dans votre projet + +### Installation manuelle +```bash +# 1. Copier les fichiers +cp ErrorReporting.js yourproject/lib/ +cp trace.js yourproject/lib/ +cp trace-wrap.js yourproject/lib/ +cp logviewer.cjs yourproject/tools/ +cp logs-viewer.html yourproject/tools/ +cp log-server.cjs yourproject/tools/ + +# 2. Installer dépendances +npm install ws pino pino-pretty + +# 3. Ajouter scripts package.json +npm pkg set scripts.logs="node tools/logviewer.cjs" +npm pkg set scripts.logs:pretty="node tools/logviewer.cjs --pretty" +npm pkg set scripts.logs:server="node tools/log-server.cjs" +``` + +## 🧪 Test rapide + +```bash +# Lancer la démonstration +node demo.js + +# Consulter les logs générés +npm run logs:pretty + +# Interface web temps réel +npm run logs:server +# Puis ouvrir logs-viewer.html +``` + +## 💻 Utilisation dans votre code + +```javascript +const { logSh, setupTracer } = require('./lib/ErrorReporting'); + +// Logging simple +logSh('Mon application démarrée', 'INFO'); +logSh('Erreur détectée', 'ERROR'); + +// Traçage hiérarchique +const tracer = setupTracer('MonModule'); +await tracer.run('maFonction', async () => { + logSh('▶ Début opération', 'TRACE'); + // ... votre code + logSh('✔ Opération terminée', 'TRACE'); +}, { param1: 'value1' }); +``` + +## 🎨 Fonctionnalités principales + +### 📊 Multi-output +- **Console** : Formatage coloré en temps réel +- **Fichier** : JSON structuré dans `logs/seo-generator-YYYY-MM-DD_HH-MM-SS.log` +- **WebSocket** : Diffusion temps réel pour interface web + +### 🌲 Traçage hiérarchique +- Suivi d'exécution avec AsyncLocalStorage +- Paramètres de fonction capturés +- Durées de performance +- Symboles visuels (▶ ✔ ✖) + +### 🔍 Consultation des logs +- **CLI** : `npm run logs:pretty` +- **Web** : Interface temps réel avec filtrage +- **Recherche** : Par niveau, mot-clé, date, module + +### 🎯 Niveaux intelligents +- **TRACE** : Flux d'exécution détaillé +- **DEBUG** : Information de débogage +- **INFO** : Événements importants +- **WARN** : Situations inhabituelles +- **ERROR** : Erreurs avec stack traces + +## 🔧 Configuration + +### Variables d'environnement +```bash +LOG_LEVEL=DEBUG # Niveau minimum (défaut: INFO) +WEBSOCKET_PORT=8081 # Port WebSocket (défaut: 8081) +ENABLE_CONSOLE_LOG=true # Console output (défaut: false) +``` + +### Personnalisation avancée +Modifier directement `ErrorReporting.js` pour: +- Changer les couleurs console +- Ajouter des champs de log personnalisés +- Modifier le format des fichiers +- Personnaliser les niveaux de log + +## 📈 Intégration production + +1. **Rotation des logs** : Utiliser `logrotate` ou équivalent +2. **Monitoring** : Interface web pour surveillance temps réel +3. **Alerting** : Parser les logs ERROR pour notifications +4. **Performance** : Logs TRACE désactivables en production + +## 🎯 Avantages de cet export + +✅ **Standalone** - Aucune dépendance Google Sheets +✅ **Portable** - Fonctionne dans n'importe quel projet Node.js +✅ **Complet** - Toutes les fonctionnalités logging préservées +✅ **Documenté** - Guide complet d'installation et d'usage +✅ **Démonstration** - Exemples concrets inclus +✅ **Production-ready** - Optimisé pour usage professionnel + +## 📞 Support + +Ce système de logging est extrait du SEO Generator et fonctionne de manière autonome. +Toutes les fonctionnalités de logging, traçage et visualisation sont opérationnelles. + +**Documentation complète** : Voir `README.md` +**Démonstration** : Lancer `node demo.js` +**Test rapide** : Lancer `node install.js` puis `npm run logs:pretty` + +--- + +🎉 **Votre système de logging professionnel est prêt !** 🎉 \ No newline at end of file diff --git a/export_logger/ErrorReporting.js b/export_logger/ErrorReporting.js new file mode 100644 index 0000000..cced259 --- /dev/null +++ b/export_logger/ErrorReporting.js @@ -0,0 +1,547 @@ +// ======================================== +// FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS +// Description: Système de validation et rapport d'erreur +// ======================================== + +// Lazy loading des modules externes +let nodemailer; +const fs = require('fs').promises; +const path = require('path'); +const pino = require('pino'); +const pretty = require('pino-pretty'); +const { PassThrough } = require('stream'); +const WebSocket = require('ws'); + +// Configuration (Google Sheets logging removed) + +// WebSocket server for real-time logs +let wsServer; +const wsClients = new Set(); + +// Enhanced Pino logger configuration with real-time streaming and dated files +const now = new Date(); +const timestamp = now.toISOString().slice(0, 10) + '_' + + now.toLocaleTimeString('fr-FR').replace(/:/g, '-'); +const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`); + +const prettyStream = pretty({ + colorize: true, + translateTime: 'HH:MM:ss.l', + ignore: 'pid,hostname', +}); + +const tee = new PassThrough(); +// Lazy loading des pipes console (évite blocage à l'import) +let consolePipeInitialized = false; + +// File destination with dated filename - FORCE DEBUG LEVEL +const fileDest = pino.destination({ + dest: logFile, + mkdir: true, + sync: false, + minLength: 0 // Force immediate write even for small logs +}); +tee.pipe(fileDest); + +// Custom levels for Pino to include TRACE, PROMPT, and LLM +const customLevels = { + trace: 5, // Below debug (10) + debug: 10, + info: 20, + prompt: 25, // New level for prompts (between info and warn) + llm: 26, // New level for LLM interactions (between prompt and warn) + warn: 30, + error: 40, + fatal: 50 +}; + +// Pino logger instance with enhanced configuration and custom levels +const logger = pino( + { + level: 'debug', // FORCE DEBUG LEVEL for file logging + base: undefined, + timestamp: pino.stdTimeFunctions.isoTime, + customLevels: customLevels, + useOnlyCustomLevels: true + }, + tee +); + +// Initialize WebSocket server (only when explicitly requested) +function initWebSocketServer() { + if (!wsServer && process.env.ENABLE_LOG_WS === 'true') { + try { + const logPort = process.env.LOG_WS_PORT || 8082; + wsServer = new WebSocket.Server({ port: logPort }); + + wsServer.on('connection', (ws) => { + wsClients.add(ws); + logger.info('Client connected to log WebSocket'); + + ws.on('close', () => { + wsClients.delete(ws); + logger.info('Client disconnected from log WebSocket'); + }); + + ws.on('error', (error) => { + logger.error('WebSocket error:', error.message); + wsClients.delete(ws); + }); + }); + + wsServer.on('error', (error) => { + if (error.code === 'EADDRINUSE') { + logger.warn(`WebSocket port ${logPort} already in use`); + wsServer = null; + } else { + logger.error('WebSocket server error:', error.message); + } + }); + + logger.info(`Log WebSocket server started on port ${logPort}`); + } catch (error) { + logger.warn(`Failed to start WebSocket server: ${error.message}`); + wsServer = null; + } + } +} + +// Broadcast log to WebSocket clients +function broadcastLog(message, level) { + const logData = { + timestamp: new Date().toISOString(), + level: level.toUpperCase(), + message: message + }; + + wsClients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify(logData)); + } catch (error) { + logger.error('Failed to send log to WebSocket client:', error.message); + wsClients.delete(ws); + } + } + }); +} + +// 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp) +// Google Sheets integration removed for export + +async function logSh(message, level = 'INFO') { + // Initialize WebSocket server if not already done + if (!wsServer) { + initWebSocketServer(); + } + + // Initialize console pipe if needed (lazy loading) + if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') { + tee.pipe(prettyStream).pipe(process.stdout); + consolePipeInitialized = true; + } + + // Convert level to lowercase for Pino + const pinoLevel = level.toLowerCase(); + + // Enhanced trace metadata for hierarchical logging + const traceData = {}; + if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) { + traceData.trace = true; + traceData.evt = message.includes('▶') ? 'span.start' : + message.includes('✔') ? 'span.end' : + message.includes('✖') ? 'span.error' : 'span.event'; + } + + // Log with Pino (handles console output with pretty formatting and file logging) + switch (pinoLevel) { + case 'error': + logger.error(traceData, message); + break; + case 'warning': + case 'warn': + logger.warn(traceData, message); + break; + case 'debug': + logger.debug(traceData, message); + break; + case 'trace': + logger.trace(traceData, message); + break; + case 'prompt': + logger.prompt(traceData, message); + break; + case 'llm': + logger.llm(traceData, message); + break; + default: + logger.info(traceData, message); + } + + // Broadcast to WebSocket clients for real-time viewing + broadcastLog(message, level); + + // Force immediate flush to ensure real-time display and prevent log loss + logger.flush(); + + // Google Sheets logging removed for export +} + +// Fonction pour déterminer si on doit logger en console +function shouldLogToConsole(messageLevel, configLevel) { + const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 }; + return levels[messageLevel] >= levels[configLevel]; +} + +// Log to file is now handled by Pino transport +// This function is kept for compatibility but does nothing +async function logToFile(message, level) { + // Pino handles file logging via transport configuration + // This function is deprecated and kept for compatibility only +} + +// 🔄 NODE.JS : Log vers Google Sheets (version async) +// Google Sheets logging functions removed for export + +// 🔄 NODE.JS : Version simplifiée cleanLogSheet +async function cleanLogSheet() { + try { + logSh('🧹 Nettoyage logs...', 'INFO'); + + // 1. Nettoyer fichiers logs locaux (garder 7 derniers jours) + await cleanLocalLogs(); + + logSh('✅ Logs nettoyés', 'INFO'); + + } catch (error) { + logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); + } +} + +async function cleanLocalLogs() { + try { + // Note: With Pino, log files are managed differently + // This function is kept for compatibility with Google Sheets logs cleanup + // Pino log rotation should be handled by external tools like logrotate + + // For now, we keep the basic cleanup for any remaining old log files + const logsDir = path.join(__dirname, '../logs'); + + try { + const files = await fs.readdir(logsDir); + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours + + for (const file of files) { + if (file.endsWith('.log')) { + const filePath = path.join(logsDir, file); + const stats = await fs.stat(filePath); + + if (stats.mtime < cutoffDate) { + await fs.unlink(filePath); + logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO'); + } + } + } + } catch (error) { + // Directory might not exist, that's fine + } + } catch (error) { + // Silent fail + } +} + +// cleanGoogleSheetsLogs function removed for export + +// ============= VALIDATION PRINCIPALE - IDENTIQUE ============= + +function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) { + logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log + + const errors = []; + const warnings = []; + const stats = { + elementsExtracted: elements.length, + contentGenerated: Object.keys(generatedContent).length, + tagsReplaced: 0, + tagsRemaining: 0 + }; + + // TEST 1: Détection tags dupliqués + const duplicateCheck = detectDuplicateTags(elements); + if (duplicateCheck.hasDuplicates) { + errors.push({ + type: 'DUPLICATE_TAGS', + severity: 'HIGH', + message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`, + impact: 'Certains contenus ne seront pas remplacés dans le XML final', + suggestion: 'Vérifier le template XML pour corriger la structure' + }); + } + + // TEST 2: Cohérence éléments extraits vs générés + const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]); + if (missingGeneration.length > 0) { + errors.push({ + type: 'MISSING_GENERATION', + severity: 'HIGH', + message: `${missingGeneration.length} éléments extraits mais non générés`, + details: missingGeneration.map(el => el.originalTag), + impact: 'Contenu incomplet dans le XML final' + }); + } + + // TEST 3: Tags non remplacés dans XML final + const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []); + stats.tagsRemaining = remainingTags.length; + + if (remainingTags.length > 0) { + errors.push({ + type: 'UNREPLACED_TAGS', + severity: 'HIGH', + message: `${remainingTags.length} tags non remplacés dans le XML final`, + details: remainingTags.slice(0, 5), + impact: 'XML final contient des placeholders non remplacés' + }); + } + + // TEST 4: Variables CSV manquantes + const missingVars = detectMissingCSVVariables(csvData); + if (missingVars.length > 0) { + warnings.push({ + type: 'MISSING_CSV_VARIABLES', + severity: 'MEDIUM', + message: `Variables CSV manquantes: ${missingVars.join(', ')}`, + impact: 'Système de génération de mots-clés automatique activé' + }); + } + + // TEST 5: Qualité génération IA + const generationQuality = assessGenerationQuality(generatedContent); + if (generationQuality.errorRate > 0.1) { + warnings.push({ + type: 'GENERATION_QUALITY', + severity: 'MEDIUM', + message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`, + impact: 'Qualité du contenu potentiellement dégradée' + }); + } + + // CALCUL STATS FINALES + stats.tagsReplaced = elements.length - remainingTags.length; + stats.successRate = stats.elementsExtracted > 0 ? + ((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100'; + + const report = { + timestamp: new Date().toISOString(), + csvData: { mc0: csvData.mc0, t0: csvData.t0 }, + stats: stats, + errors: errors, + warnings: warnings, + status: errors.length === 0 ? 'SUCCESS' : 'ERROR' + }; + + const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR'; + logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log + + // ENVOYER RAPPORT SI ERREURS (async en arrière-plan) + if (errors.length > 0 || warnings.length > 2) { + sendErrorReport(report).catch(err => { + logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error + }); + } + + return report; +} + +// ============= HELPERS - IDENTIQUES ============= + +function detectDuplicateTags(elements) { + const tagCounts = {}; + const duplicates = []; + + elements.forEach(element => { + const tag = element.originalTag; + tagCounts[tag] = (tagCounts[tag] || 0) + 1; + + if (tagCounts[tag] === 2) { + duplicates.push(tag); + logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error + } + }); + + return { + hasDuplicates: duplicates.length > 0, + duplicates: duplicates, + counts: tagCounts + }; +} + +function detectMissingCSVVariables(csvData) { + const missing = []; + + if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) { + missing.push('MC+1 (insuffisant)'); + } + if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) { + missing.push('T+1 (insuffisant)'); + } + if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) { + missing.push('L+1 (insuffisant)'); + } + + return missing; +} + +function assessGenerationQuality(generatedContent) { + let errorCount = 0; + let totalCount = Object.keys(generatedContent).length; + + Object.values(generatedContent).forEach(content => { + if (content && ( + content.includes('[ERREUR') || + content.includes('ERROR') || + content.length < 10 + )) { + errorCount++; + } + }); + + return { + errorRate: totalCount > 0 ? errorCount / totalCount : 0, + totalGenerated: totalCount, + errorsFound: errorCount + }; +} + +// 🔄 NODE.JS : Email avec nodemailer (remplace MailApp) +async function sendErrorReport(report) { + try { + logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log + + // Lazy load nodemailer seulement quand nécessaire + if (!nodemailer) { + nodemailer = require('nodemailer'); + } + + // Configuration nodemailer (Gmail par exemple) + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, // 'your-email@gmail.com' + pass: process.env.EMAIL_APP_PASSWORD // App password Google + } + }); + + const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`; + const htmlBody = createHTMLReport(report); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: 'alexistrouve.pro@gmail.com', + subject: subject, + html: htmlBody, + attachments: [{ + filename: `error-report-${Date.now()}.json`, + content: JSON.stringify(report, null, 2), + contentType: 'application/json' + }] + }; + + await transporter.sendMail(mailOptions); + logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log + + } catch (error) { + logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error + } +} + +// ============= HTML REPORT - IDENTIQUE ============= + +function createHTMLReport(report) { + const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545'; + + let html = ` +
+

Rapport Workflow SEO Automatisé (Node.js)

+ +
+

Résumé Exécutif

+

Statut: ${report.status}

+

Article: ${report.csvData.t0}

+

Mot-clé: ${report.csvData.mc0}

+

Taux de réussite: ${report.stats.successRate}%

+

Timestamp: ${report.timestamp}

+

Plateforme: Node.js Server

+
`; + + if (report.errors.length > 0) { + html += `
+

Erreurs Critiques (${report.errors.length})

`; + + report.errors.forEach((error, i) => { + html += ` +
+

${i + 1}. ${error.type}

+

Message: ${error.message}

+

Impact: ${error.impact}

+ ${error.suggestion ? `

Solution: ${error.suggestion}

` : ''} +
`; + }); + + html += `
`; + } + + if (report.warnings.length > 0) { + html += `
+

Avertissements (${report.warnings.length})

`; + + report.warnings.forEach((warning, i) => { + html += ` +
+

${i + 1}. ${warning.type}

+

${warning.message}

+
`; + }); + + html += `
`; + } + + html += ` +
+

Statistiques Détaillées

+
    +
  • Éléments extraits: ${report.stats.elementsExtracted}
  • +
  • Contenus générés: ${report.stats.contentGenerated}
  • +
  • Tags remplacés: ${report.stats.tagsReplaced}
  • +
  • Tags restants: ${report.stats.tagsRemaining}
  • +
+
+ +
+

Informations Système

+
    +
  • Plateforme: Node.js
  • +
  • Version: ${process.version}
  • +
  • Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB
  • +
  • Uptime: ${Math.round(process.uptime())}s
  • +
+
+
`; + + return html; +} + +// 🔄 NODE.JS EXPORTS +module.exports = { + logSh, + setupTracer: require('./trace').setupTracer, + cleanLogSheet, + validateWorkflowIntegrity, + detectDuplicateTags, + detectMissingCSVVariables, + assessGenerationQuality, + sendErrorReport, + createHTMLReport, + initWebSocketServer +}; \ No newline at end of file diff --git a/export_logger/README.md b/export_logger/README.md new file mode 100644 index 0000000..e086bfd --- /dev/null +++ b/export_logger/README.md @@ -0,0 +1,310 @@ +# 📋 Système de Logging SEO Generator + +Système de logging centralisé avec support multi-output (Console + File + WebSocket) et visualisation en temps réel. + +## 🏗️ Architecture + +### Composants principaux + +1. **ErrorReporting.js** - Système de logging centralisé avec `logSh()` +2. **trace.js** - Système de traçage hiérarchique avec AsyncLocalStorage +3. **trace-wrap.js** - Utilitaires de wrapping pour le tracing +4. **logviewer.cjs** - Outil CLI pour consulter les logs +5. **logs-viewer.html** - Interface web temps réel +6. **log-server.cjs** - Serveur WebSocket pour logs temps réel + +## 🚀 Installation + +### 1. Copier les fichiers + +```bash +# Dans votre projet Node.js +cp ErrorReporting.js lib/ +cp trace.js lib/ +cp trace-wrap.js lib/ +cp logviewer.cjs tools/ +cp logs-viewer.html tools/ +cp log-server.cjs tools/ +``` + +### 2. Installer les dépendances + +```bash +npm install ws edge-runtime +``` + +### 3. Configuration package.json + +```json +{ + "scripts": { + "logs": "node tools/logviewer.cjs", + "logs:server": "node tools/log-server.cjs", + "logs:pretty": "node tools/logviewer.cjs --pretty" + } +} +``` + +## 📝 Utilisation + +### 1. Dans votre code + +```javascript +// Import principal +const { logSh, setupTracer } = require('./lib/ErrorReporting'); + +// Configuration du traceur (optionnel) +const tracer = setupTracer('MonModule'); + +// Utilisation basique +logSh('Message info', 'INFO'); +logSh('Erreur détectée', 'ERROR'); +logSh('Debug info', 'DEBUG'); + +// Avec traçage hiérarchique +await tracer.run('maFonction', async () => { + logSh('Début opération', 'TRACE'); + // ... votre code + logSh('Fin opération', 'TRACE'); +}, { param1: 'value1' }); +``` + +### 2. Consultation des logs + +#### Via CLI +```bash +# Logs récents avec formatage +npm run logs:pretty + +# Recherche par mot-clé +node tools/logviewer.cjs --search --includes "ERROR" --pretty + +# Filtrer par niveau +node tools/logviewer.cjs --level ERROR --pretty + +# Plage temporelle +node tools/logviewer.cjs --since 2025-01-01T00:00:00Z --until 2025-01-01T23:59:59Z +``` + +#### Via interface web +```bash +# Lancer le serveur WebSocket +npm run logs:server + +# Ouvrir logs-viewer.html dans un navigateur +# L'interface se connecte automatiquement sur ws://localhost:8081 +``` + +## 🎯 Fonctionnalités + +### Niveaux de logs +- **TRACE** (10) : Exécution hiérarchique avec symboles ▶ ✔ ✖ +- **DEBUG** (20) : Information détaillée de débogage +- **INFO** (30) : Messages informatifs standard +- **WARN** (40) : Conditions d'avertissement +- **ERROR** (50) : Conditions d'erreur avec stack traces + +### Outputs multiples +- **Console** : Formatage coloré avec timestamps +- **Fichier** : JSON structuré dans `logs/app-YYYY-MM-DD_HH-MM-SS.log` +- **WebSocket** : Diffusion temps réel pour interface web + +### Traçage hiérarchique +```javascript +const tracer = setupTracer('MonModule'); + +await tracer.run('operationPrincipale', async () => { + logSh('▶ Début opération principale', 'TRACE'); + + await tracer.run('sousOperation', async () => { + logSh('▶ Début sous-opération', 'TRACE'); + // ... code + logSh('✔ Sous-opération terminée', 'TRACE'); + }, { subParam: 'value' }); + + logSh('✔ Opération principale terminée', 'TRACE'); +}, { mainParam: 'value' }); +``` + +## 🔧 Configuration + +### Variables d'environnement + +```bash +# Niveau de log minimum (défaut: INFO) +LOG_LEVEL=DEBUG + +# Port WebSocket (défaut: 8081) +WEBSOCKET_PORT=8081 + +# Répertoire des logs (défaut: logs/) +LOG_DIRECTORY=logs +``` + +### Personnalisation ErrorReporting.js + +```javascript +// Modifier les couleurs console +const COLORS = { + TRACE: '\x1b[90m', // Gris + DEBUG: '\x1b[34m', // Bleu + INFO: '\x1b[32m', // Vert + WARN: '\x1b[33m', // Jaune + ERROR: '\x1b[31m' // Rouge +}; + +// Modifier le format de fichier +const logEntry = { + level: numericLevel, + time: new Date().toISOString(), + msg: message, + // Ajouter des champs personnalisés + module: 'MonModule', + userId: getCurrentUserId() +}; +``` + +## 📊 Interface Web (logs-viewer.html) + +### Fonctionnalités +- ✅ **Logs temps réel** via WebSocket +- ✅ **Filtrage par niveau** (TRACE, DEBUG, INFO, WARN, ERROR) +- ✅ **Recherche textuelle** dans les messages +- ✅ **Auto-scroll** avec possibilité de pause +- ✅ **Formatage coloré** selon niveau +- ✅ **Timestamps lisibles** + +### Utilisation +1. Lancer le serveur WebSocket : `npm run logs:server` +2. Ouvrir `logs-viewer.html` dans un navigateur +3. L'interface se connecte automatiquement et affiche les logs + +## 🛠️ Outils CLI + +### logviewer.cjs + +```bash +# Options disponibles +--pretty # Formatage coloré et lisible +--last N # N dernières lignes (défaut: 200) +--level LEVEL # Filtrer par niveau (TRACE, DEBUG, INFO, WARN, ERROR) +--includes TEXT # Rechercher TEXT dans les messages +--regex PATTERN # Recherche par expression régulière +--since DATE # Logs depuis cette date (ISO ou YYYY-MM-DD) +--until DATE # Logs jusqu'à cette date +--module MODULE # Filtrer par module +--search # Mode recherche interactif + +# Exemples +node tools/logviewer.cjs --last 100 --level ERROR --pretty +node tools/logviewer.cjs --search --includes "Claude" --pretty +node tools/logviewer.cjs --since 2025-01-15 --pretty +``` + +## 🎨 Exemples d'usage + +### Logging simple +```javascript +const { logSh } = require('./lib/ErrorReporting'); + +// Messages informatifs +logSh('Application démarrée', 'INFO'); +logSh('Utilisateur connecté: john@example.com', 'DEBUG'); + +// Gestion d'erreurs +try { + // ... code risqué +} catch (error) { + logSh(`Erreur lors du traitement: ${error.message}`, 'ERROR'); +} +``` + +### Traçage de fonction complexe +```javascript +const { logSh, setupTracer } = require('./lib/ErrorReporting'); +const tracer = setupTracer('UserService'); + +async function processUser(userId) { + return await tracer.run('processUser', async () => { + logSh(`▶ Traitement utilisateur ${userId}`, 'TRACE'); + + const user = await tracer.run('fetchUser', async () => { + logSh('▶ Récupération données utilisateur', 'TRACE'); + const userData = await database.getUser(userId); + logSh('✔ Données utilisateur récupérées', 'TRACE'); + return userData; + }, { userId }); + + await tracer.run('validateUser', async () => { + logSh('▶ Validation données utilisateur', 'TRACE'); + validateUserData(user); + logSh('✔ Données utilisateur validées', 'TRACE'); + }, { userId, userEmail: user.email }); + + logSh('✔ Traitement utilisateur terminé', 'TRACE'); + return user; + }, { userId }); +} +``` + +## 🚨 Bonnes pratiques + +### 1. Niveaux appropriés +- **TRACE** : Flux d'exécution détaillé (entrée/sortie fonctions) +- **DEBUG** : Information de débogage (variables, états) +- **INFO** : Événements importants (démarrage, connexions) +- **WARN** : Situations inhabituelles mais gérables +- **ERROR** : Erreurs nécessitant attention + +### 2. Messages structurés +```javascript +// ✅ Bon +logSh(`Utilisateur ${userId} connecté depuis ${ip}`, 'INFO'); + +// ❌ Éviter +logSh('Un utilisateur s\'est connecté', 'INFO'); +``` + +### 3. Gestion des erreurs +```javascript +// ✅ Avec contexte +try { + await processPayment(orderId); +} catch (error) { + logSh(`Erreur traitement paiement commande ${orderId}: ${error.message}`, 'ERROR'); + logSh(`Stack trace: ${error.stack}`, 'DEBUG'); +} +``` + +### 4. Performance +```javascript +// ✅ Éviter logs trop fréquents en production +if (process.env.NODE_ENV === 'development') { + logSh(`Variable debug: ${JSON.stringify(complexObject)}`, 'DEBUG'); +} +``` + +## 📦 Structure des fichiers de logs + +``` +logs/ +├── app-2025-01-15_10-30-45.log # Logs JSON structurés +├── app-2025-01-15_14-22-12.log +└── ... +``` + +Format JSON par ligne : +```json +{"level":20,"time":"2025-01-15T10:30:45.123Z","msg":"Message de log"} +{"level":30,"time":"2025-01-15T10:30:46.456Z","msg":"Autre message","module":"UserService","traceId":"abc123"} +``` + +## 🔄 Intégration dans projet existant + +1. **Remplacer console.log** par `logSh()` +2. **Ajouter traçage** aux fonctions critiques +3. **Configurer niveaux** selon environnement +4. **Mettre en place monitoring** avec interface web +5. **Automatiser consultation** des logs via CLI + +Ce système de logging vous donnera une visibilité complète sur le comportement de votre application ! 🎯 \ No newline at end of file diff --git a/export_logger/demo.js b/export_logger/demo.js new file mode 100644 index 0000000..b1f6d78 --- /dev/null +++ b/export_logger/demo.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node +// ======================================== +// DÉMONSTRATION - SYSTÈME DE LOGGING +// Description: Démo complète des fonctionnalités du système de logging +// ======================================== + +const { logSh, setupTracer } = require('./ErrorReporting'); + +// Configuration du traceur pour cette démo +const tracer = setupTracer('DemoModule'); + +console.log(` +╔════════════════════════════════════════════════════════════╗ +║ 🎬 DÉMONSTRATION LOGGING ║ +║ Toutes les fonctionnalités en action ║ +╚════════════════════════════════════════════════════════════╝ +`); + +async function demonstrationComplete() { + // 1. DÉMONSTRATION DES NIVEAUX DE LOG + console.log('\n📋 1. DÉMONSTRATION DES NIVEAUX DE LOG'); + + logSh('Message de trace pour débuggage détaillé', 'TRACE'); + logSh('Message de debug avec informations techniques', 'DEBUG'); + logSh('Message informatif standard', 'INFO'); + logSh('Message d\'avertissement - situation inhabituelle', 'WARN'); + logSh('Message d\'erreur - problème détecté', 'ERROR'); + + await sleep(1000); + + // 2. DÉMONSTRATION DU TRAÇAGE HIÉRARCHIQUE + console.log('\n🌲 2. DÉMONSTRATION DU TRAÇAGE HIÉRARCHIQUE'); + + await tracer.run('operationPrincipale', async () => { + logSh('▶ Début opération principale', 'TRACE'); + + await tracer.run('preparationDonnees', async () => { + logSh('▶ Préparation des données', 'TRACE'); + await sleep(500); + logSh('✔ Données préparées', 'TRACE'); + }, { dataSize: '1MB', format: 'JSON' }); + + await tracer.run('traitementDonnees', async () => { + logSh('▶ Traitement des données', 'TRACE'); + + await tracer.run('validation', async () => { + logSh('▶ Validation en cours', 'TRACE'); + await sleep(300); + logSh('✔ Validation réussie', 'TRACE'); + }, { rules: 15, passed: 15 }); + + await tracer.run('transformation', async () => { + logSh('▶ Transformation des données', 'TRACE'); + await sleep(400); + logSh('✔ Transformation terminée', 'TRACE'); + }, { inputFormat: 'JSON', outputFormat: 'XML' }); + + logSh('✔ Traitement terminé', 'TRACE'); + }, { records: 1500 }); + + logSh('✔ Opération principale terminée', 'TRACE'); + }, { operationId: 'OP-2025-001', priority: 'high' }); + + await sleep(1000); + + // 3. DÉMONSTRATION DE LA GESTION D'ERREURS + console.log('\n🚨 3. DÉMONSTRATION DE LA GESTION D\'ERREURS'); + + await tracer.run('operationAvecErreur', async () => { + logSh('▶ Tentative d\'opération risquée', 'TRACE'); + + try { + await simulerErreur(); + } catch (error) { + logSh(`✖ Erreur capturée: ${error.message}`, 'ERROR'); + logSh(`Stack trace: ${error.stack}`, 'DEBUG'); + } + + logSh('✔ Récupération d\'erreur gérée', 'TRACE'); + }, { attemptNumber: 1 }); + + await sleep(1000); + + // 4. DÉMONSTRATION DES MESSAGES CONTEXTUELS + console.log('\n🎯 4. DÉMONSTRATION DES MESSAGES CONTEXTUELS'); + + const userId = 'user123'; + const orderId = 'ORD-456'; + + await tracer.run('traitementCommande', async () => { + logSh(`▶ Début traitement commande ${orderId} pour utilisateur ${userId}`, 'TRACE'); + + logSh(`Validation utilisateur ${userId}`, 'DEBUG'); + logSh(`Utilisateur ${userId} validé avec succès`, 'INFO'); + + logSh(`Calcul du montant pour commande ${orderId}`, 'DEBUG'); + logSh(`Montant calculé: 125.50€ pour commande ${orderId}`, 'INFO'); + + logSh(`Traitement paiement commande ${orderId}`, 'DEBUG'); + logSh(`Paiement confirmé pour commande ${orderId}`, 'INFO'); + + logSh(`✔ Commande ${orderId} traitée avec succès`, 'TRACE'); + }, { userId, orderId, amount: 125.50 }); + + await sleep(1000); + + // 5. DÉMONSTRATION DES LOGS TECHNIQUES + console.log('\n⚙️ 5. DÉMONSTRATION DES LOGS TECHNIQUES'); + + await tracer.run('operationTechnique', async () => { + logSh('▶ Connexion base de données', 'TRACE'); + logSh('Paramètres connexion: host=localhost, port=5432, db=produit', 'DEBUG'); + logSh('Connexion BDD établie', 'INFO'); + + logSh('▶ Exécution requête complexe', 'TRACE'); + logSh('SQL: SELECT * FROM users WHERE active = true AND last_login > ?', 'DEBUG'); + logSh('Requête exécutée en 45ms, 234 résultats', 'INFO'); + + logSh('▶ Mise en cache des résultats', 'TRACE'); + logSh('Cache key: users_active_recent, TTL: 300s', 'DEBUG'); + logSh('Données mises en cache', 'INFO'); + + logSh('✔ Opération technique terminée', 'TRACE'); + }, { dbHost: 'localhost', cacheSize: '2.3MB' }); + + await sleep(1000); + + // 6. DÉMONSTRATION DES LOGS PERFORMANCE + console.log('\n🏃 6. DÉMONSTRATION DES LOGS PERFORMANCE'); + + const startTime = Date.now(); + + await tracer.run('operationPerformance', async () => { + logSh('▶ Début opération critique performance', 'TRACE'); + + for (let i = 1; i <= 5; i++) { + await tracer.run(`etape${i}`, async () => { + logSh(`▶ Étape ${i}/5`, 'TRACE'); + const stepStart = Date.now(); + + await sleep(100 + Math.random() * 200); // Simule du travail variable + + const stepDuration = Date.now() - stepStart; + logSh(`✔ Étape ${i} terminée en ${stepDuration}ms`, 'TRACE'); + }, { step: i, total: 5 }); + } + + const totalDuration = Date.now() - startTime; + logSh(`✔ Opération terminée en ${totalDuration}ms`, 'TRACE'); + + if (totalDuration > 1000) { + logSh(`Performance dégradée: ${totalDuration}ms > 1000ms`, 'WARN'); + } else { + logSh('Performance satisfaisante', 'INFO'); + } + }, { expectedDuration: '800ms', actualDuration: `${Date.now() - startTime}ms` }); + + // RÉSUMÉ FINAL + console.log(` +╔════════════════════════════════════════════════════════════╗ +║ ✅ DÉMONSTRATION TERMINÉE ║ +╚════════════════════════════════════════════════════════════╝ + +🎯 Vous avez vu en action: + • Niveaux de logs (TRACE, DEBUG, INFO, WARN, ERROR) + • Traçage hiérarchique avec contexte + • Gestion d'erreurs structurée + • Messages contextuels avec IDs + • Logs techniques détaillés + • Monitoring de performance + +📊 Consulter les logs générés: + npm run logs:pretty + +🌐 Interface temps réel: + npm run logs:server + # Puis ouvrir tools/logs-viewer.html + +🔍 Rechercher dans les logs: + npm run logs:search + +Le système de logging est maintenant configuré et opérationnel ! 🚀 +`); +} + +async function simulerErreur() { + await sleep(200); + throw new Error('Connexion base de données impossible - timeout après 5000ms'); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Lancer la démonstration +if (require.main === module) { + demonstrationComplete().catch(err => { + logSh(`Erreur dans la démonstration: ${err.message}`, 'ERROR'); + process.exit(1); + }); +} + +module.exports = { demonstrationComplete }; \ No newline at end of file diff --git a/export_logger/log-server.cjs b/export_logger/log-server.cjs new file mode 100644 index 0000000..d950ad2 --- /dev/null +++ b/export_logger/log-server.cjs @@ -0,0 +1,179 @@ +#!/usr/bin/env node +// tools/log-server.js - Serveur simple pour visualiser les logs +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); + +const app = express(); +const PORT = 3001; + +// Servir les fichiers statiques depuis la racine du projet +app.use(express.static(path.join(__dirname, '..'))); + +// Route pour servir les fichiers de log +app.use('/logs', express.static(path.join(__dirname, '..', 'logs'))); + +// Liste des fichiers de log disponibles +app.get('/api/logs', (req, res) => { + try { + const logsDir = path.join(__dirname, '..', 'logs'); + const files = fs.readdirSync(logsDir) + .filter(file => file.endsWith('.log')) + .map(file => { + const filePath = path.join(logsDir, file); + const stats = fs.statSync(filePath); + return { + name: file, + size: stats.size, + modified: stats.mtime.toISOString(), + url: `http://localhost:${PORT}/tools/logs-viewer.html?file=${file}` + }; + }) + .sort((a, b) => new Date(b.modified) - new Date(a.modified)); + + res.json({ files }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Page d'accueil avec liste des logs +app.get('/', (req, res) => { + res.send(` + + + + Log Viewer Server + + + +

📊 SEO Generator - Log Viewer

+ + 🔴 Logs en temps réel + +
+

Fichiers de log disponibles

+
Chargement...
+
+ + + + + `); +}); + +// Fonction pour ouvrir automatiquement le dernier log +function openLatestLog() { + try { + const logsDir = path.join(__dirname, '..', 'logs'); + const files = fs.readdirSync(logsDir) + .filter(file => file.endsWith('.log')) + .map(file => { + const filePath = path.join(logsDir, file); + const stats = fs.statSync(filePath); + return { + name: file, + modified: stats.mtime + }; + }) + .sort((a, b) => b.modified - a.modified); + + if (files.length > 0) { + const latestFile = files[0].name; + const url = `http://localhost:${PORT}/tools/logs-viewer.html?file=${latestFile}`; + + // Ouvrir dans le navigateur par défaut + // Utiliser powershell Start-Process pour ouvrir l'URL dans le navigateur + const command = 'powershell.exe Start-Process'; + + exec(`${command} "${url}"`, (error) => { + if (error) { + console.log(`⚠️ Impossible d'ouvrir automatiquement: ${error.message}`); + console.log(`🌐 Ouvrez manuellement: ${url}`); + } else { + console.log(`🌐 Ouverture automatique du dernier log: ${latestFile}`); + } + }); + } else { + console.log(`📊 Aucun log disponible - accédez à http://localhost:${PORT}/tools/logs-viewer.html`); + } + } catch (error) { + console.log(`⚠️ Erreur lors de l'ouverture: ${error.message}`); + } +} + +app.listen(PORT, () => { + console.log(`🚀 Log server running at http://localhost:${PORT}`); + console.log(`📊 Logs viewer: http://localhost:${PORT}/tools/logs-viewer.html`); + console.log(`📁 Logs directory: ${path.join(__dirname, '..', 'logs')}`); + + // Attendre un peu que le serveur soit prêt, puis ouvrir le navigateur + setTimeout(openLatestLog, 1000); +}); \ No newline at end of file diff --git a/export_logger/logs-viewer.html b/export_logger/logs-viewer.html new file mode 100644 index 0000000..abd9736 --- /dev/null +++ b/export_logger/logs-viewer.html @@ -0,0 +1,928 @@ + + + + + + SEO Generator - Logs en temps réel + + + +
+
+

SEO Generator - Logs temps réel

+ Connexion... + Port: 8082 +
+ + +
+
+
+ Filtres: + + + + + + + +
+ + + +
+
+ +
+ +
0 résultats
+ + + +
+ +
+
+ --:--:-- + INFO + En attente des logs... +
+
+ + + + \ No newline at end of file diff --git a/export_logger/logviewer.cjs b/export_logger/logviewer.cjs new file mode 100644 index 0000000..7571ef4 --- /dev/null +++ b/export_logger/logviewer.cjs @@ -0,0 +1,338 @@ +// tools/logViewer.js (Pino-compatible JSONL + timearea + filters) +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const readline = require('readline'); + +function resolveLatestLogFile(dir = path.resolve(process.cwd(), 'logs')) { + if (!fs.existsSync(dir)) throw new Error(`Logs directory not found: ${dir}`); + const files = fs.readdirSync(dir) + .map(f => ({ file: f, stat: fs.statSync(path.join(dir, f)) })) + .filter(f => f.stat.isFile()) + .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs); + if (!files.length) throw new Error(`No log files in ${dir}`); + return path.join(dir, files[0].file); +} + +let LOG_FILE = process.env.LOG_FILE + ? path.resolve(process.cwd(), process.env.LOG_FILE) + : resolveLatestLogFile(); + +const MAX_SAFE_READ_MB = 50; +const DEFAULT_LAST_LINES = 200; + +function setLogFile(filePath) { LOG_FILE = path.resolve(process.cwd(), filePath); } + +function MB(n){return n*1024*1024;} +function toInt(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;} + +const LEVEL_MAP_NUM = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'}; +function normLevel(v){ + if (v==null) return 'UNKNOWN'; + if (typeof v==='number') return LEVEL_MAP_NUM[v]||String(v); + const s=String(v).toUpperCase(); + return LEVEL_MAP_NUM[Number(s)] || s; +} + +function parseWhen(obj){ + const t = obj.time ?? obj.timestamp; + if (t==null) return null; + if (typeof t==='number') return new Date(t); + const d=new Date(String(t)); + return isNaN(d)?null:d; +} + +function prettyLine(obj){ + const d=parseWhen(obj); + const ts = d? d.toISOString() : ''; + const lvl = normLevel(obj.level).padEnd(5,' '); + const mod = (obj.module || obj.path || obj.name || 'root').slice(0,60).padEnd(60,' '); + const msg = obj.msg ?? obj.message ?? ''; + const extra = obj.evt ? ` [${obj.evt}${obj.dur_ms?` ${obj.dur_ms}ms`:''}]` : ''; + return `${ts} ${lvl} ${mod} ${msg}${extra}`; +} + +function buildFilters({ level, mod, since, until, includes, regex, timeareaCenter, timeareaRadiusSec, filterTerms }) { + let rx=null; if (regex){ try{rx=new RegExp(regex,'i');}catch{} } + const sinceDate = since? new Date(since): null; + const untilDate = until? new Date(until): null; + const wantLvl = level? normLevel(level): null; + + // timearea : centre + rayon (en secondes) + let areaStart = null, areaEnd = null; + if (timeareaCenter && timeareaRadiusSec!=null) { + const c = new Date(timeareaCenter); + if (!isNaN(c)) { + const rMs = Number(timeareaRadiusSec) * 1000; + areaStart = new Date(c.getTime() - rMs); + areaEnd = new Date(c.getTime() + rMs); + } + } + + // terms (peuvent être multiples) : match sur msg/path/module/evt/name/attrs stringify + const terms = Array.isArray(filterTerms) ? filterTerms.filter(Boolean) : (filterTerms ? [filterTerms] : []); + + return { wantLvl, mod, sinceDate, untilDate, includes, rx, areaStart, areaEnd, terms }; +} + +function objectToSearchString(o) { + const parts = []; + if (o.msg!=null) parts.push(String(o.msg)); + if (o.message!=null) parts.push(String(o.message)); + if (o.module!=null) parts.push(String(o.module)); + if (o.path!=null) parts.push(String(o.path)); + if (o.name!=null) parts.push(String(o.name)); + if (o.evt!=null) parts.push(String(o.evt)); + if (o.span!=null) parts.push(String(o.span)); + if (o.attrs!=null) parts.push(safeStringify(o.attrs)); + return parts.join(' | ').toLowerCase(); +} + +function safeStringify(v){ try{return JSON.stringify(v);}catch{return String(v);} } + +function passesAll(obj,f){ + if (!obj || typeof obj!=='object') return false; + + if (f.wantLvl && normLevel(obj.level)!==f.wantLvl) return false; + + if (f.mod){ + const mod = String(obj.module||obj.path||obj.name||''); + if (mod!==f.mod) return false; + } + + // since/until + let d=parseWhen(obj); + if (f.sinceDate || f.untilDate){ + if (!d) return false; + if (f.sinceDate && d < f.sinceDate) return false; + if (f.untilDate && d > f.untilDate) return false; + } + + // timearea (zone centrée) + if (f.areaStart || f.areaEnd) { + if (!d) d = parseWhen(obj); + if (!d) return false; + if (f.areaStart && d < f.areaStart) return false; + if (f.areaEnd && d > f.areaEnd) return false; + } + + const msg = String(obj.msg ?? obj.message ?? ''); + if (f.includes && !msg.toLowerCase().includes(String(f.includes).toLowerCase())) return false; + if (f.rx && !f.rx.test(msg)) return false; + + // terms : tous les --filter doivent matcher (AND) + if (f.terms && f.terms.length) { + const hay = objectToSearchString(obj); // multi-champs + for (const t of f.terms) { + if (!hay.includes(String(t).toLowerCase())) return false; + } + } + + return true; +} + +function applyFilters(arr, f){ return arr.filter(o=>passesAll(o,f)); } +function safeParse(line){ try{return JSON.parse(line);}catch{return null;} } +function safeParseLines(lines){ const out=[]; for(const l of lines){const o=safeParse(l); if(o) out.push(o);} return out; } + +async function getFileSize(file){ const st=await fs.promises.stat(file).catch(()=>null); if(!st) throw new Error(`Log file not found: ${file}`); return st.size; } +async function readAllLines(file){ const data=await fs.promises.readFile(file,'utf8'); const lines=data.split(/\r?\n/).filter(Boolean); return safeParseLines(lines); } + +async function tailJsonl(file, approxLines=DEFAULT_LAST_LINES){ + const fd=await fs.promises.open(file,'r'); + try{ + const stat=await fd.stat(); const chunk=64*1024; + let pos=stat.size; let buffer=''; const lines=[]; + while(pos>0 && lines.length=limit) break; } + } + rl.close(); return out; +} + +async function streamEach(file, onObj){ + const rl=readline.createInterface({ input: fs.createReadStream(file,{encoding:'utf8'}), crlfDelay:Infinity }); + for await (const line of rl){ if(!line.trim()) continue; const o=safeParse(line); if(o) onObj(o); } + rl.close(); +} + +async function getLast(opts={}){ + const { + lines=DEFAULT_LAST_LINES, level, module:mod, since, until, includes, regex, + timeareaCenter, timeareaRadiusSec, filterTerms, pretty=false + } = opts; + + const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms}); + const size=await getFileSize(LOG_FILE); + + if (size<=MB(MAX_SAFE_READ_MB)){ + const arr=await readAllLines(LOG_FILE); + const out=applyFilters(arr.slice(-Math.max(lines,1)),filters); + return pretty? out.map(prettyLine): out; + } + const out=await tailJsonl(LOG_FILE, lines*3); + const filtered=applyFilters(out,filters).slice(-Math.max(lines,1)); + return pretty? filtered.map(prettyLine): filtered; +} + +async function search(opts={}){ + const { + limit=500, level, module:mod, since, until, includes, regex, + timeareaCenter, timeareaRadiusSec, filterTerms, pretty=false + } = opts; + + const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms}); + const size=await getFileSize(LOG_FILE); + const res = size<=MB(MAX_SAFE_READ_MB) + ? applyFilters(await readAllLines(LOG_FILE),filters).slice(-limit) + : await streamFilter(LOG_FILE,filters,limit); + return pretty? res.map(prettyLine): res; +} + +async function stats(opts={}){ + const {by='level', since, until, level, module:mod, includes, regex, timeareaCenter, timeareaRadiusSec, filterTerms}=opts; + const filters=buildFilters({level,mod,since,until,includes,regex,timeareaCenter,timeareaRadiusSec,filterTerms}); + const agg={}; + await streamEach(LOG_FILE,(o)=>{ + if(!passesAll(o,filters)) return; + let key; + if (by==='day'){ const d=parseWhen(o); if(!d) return; key=d.toISOString().slice(0,10); } + else if (by==='module'){ key= o.module || o.path || o.name || 'unknown'; } + else { key= normLevel(o.level); } + agg[key]=(agg[key]||0)+1; + }); + return Object.entries(agg).sort((a,b)=>b[1]-a[1]).map(([k,v])=>({[by]:k, count:v})); +} + +// --- CLI --- +if (require.main===module){ + (async ()=>{ + try{ + const args=parseArgs(process.argv.slice(2)); + if (args.help) return printHelp(); + if (args.file) setLogFile(args.file); + // Support for positional filename arguments + if (args.unknown && args.unknown.length > 0 && !args.file) { + const possibleFile = args.unknown[0]; + if (possibleFile && !possibleFile.startsWith('-')) { + setLogFile(possibleFile); + } + } + + const common = { + level: args.level, + module: args.module, + since: args.since, + until: args.until, + includes: args.includes, + regex: args.regex, + timeareaCenter: args.timeareaCenter, + timeareaRadiusSec: args.timeareaRadiusSec, + filterTerms: args.filterTerms, + }; + + if (args.stats){ + const res=await stats({by:args.by||'level', ...common}); + return console.log(JSON.stringify(res,null,2)); + } + if (args.search){ + const res=await search({limit:toInt(args.limit,500), ...common, pretty:!!args.pretty}); + return printResult(res,!!args.pretty); + } + const res=await getLast({lines:toInt(args.last,DEFAULT_LAST_LINES), ...common, pretty:!!args.pretty}); + return printResult(res,!!args.pretty); + }catch(e){ console.error(`[logViewer] Error: ${e.message}`); process.exitCode=1; } + })(); +} + +function parseArgs(argv){ + const o={ filterTerms: [] }; + for(let i=0;i (i+1 + case '--timearea': { + o.timeareaCenter = nx(); i++; + const radius = nx(); i++; + o.timeareaRadiusSec = radius != null ? Number(radius) : undefined; + break; + } + + // NEW: --filter (répétable) + case '--filter': { + const term = nx(); i++; + if (term!=null) o.filterTerms.push(term); + break; + } + + default: (o.unknown??=[]).push(a); + } + } + if (o.filterTerms.length===0) delete o.filterTerms; + return o; +} + +function printHelp(){ + const bin=`node ${path.relative(process.cwd(), __filename)}`; + console.log(` +LogViewer (Pino-compatible JSONL) + +Usage: + ${bin} [--file logs/app.log] [--pretty] [--last 200] [filters...] + ${bin} --search [--limit 500] [filters...] + ${bin} --stats [--by level|module|day] [filters...] + +Time filters: + --since 2025-09-02T00:00:00Z + --until 2025-09-02T23:59:59Z + --timearea # fenêtre centrée + +Text filters: + --includes "keyword in msg" + --regex "(timeout|ECONNRESET)" + --filter TERM # multi-champs (msg, path/module, name, evt, attrs). Répétable. AND. + +Other filters: + --level 30|INFO|ERROR + --module "Workflow SEO > Génération contenu multi-LLM" + +Examples: + ${bin} --timearea 2025-09-02T23:59:59Z 200 --pretty + ${bin} --timearea 2025-09-02T12:00:00Z 900 --filter INFO --filter PROMPT --search --pretty + ${bin} --last 300 --level ERROR --filter "Génération contenu" --pretty +`);} + +function printResult(res, pretty){ console.log(pretty? res.join(os.EOL) : JSON.stringify(res,null,2)); } + +module.exports = { setLogFile, getLast, search, stats }; diff --git a/export_logger/package-lock.json b/export_logger/package-lock.json new file mode 100644 index 0000000..d30532a --- /dev/null +++ b/export_logger/package-lock.json @@ -0,0 +1,441 @@ +{ + "name": "seo-generator-logger", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "seo-generator-logger", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "pino": "^8.15.0", + "pino-pretty": "^10.2.0", + "ws": "^8.14.0" + }, + "devDependencies": {}, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.3.1.tgz", + "integrity": "sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/export_logger/package.json b/export_logger/package.json new file mode 100644 index 0000000..f9cfca9 --- /dev/null +++ b/export_logger/package.json @@ -0,0 +1,42 @@ +{ + "name": "seo-generator-logger", + "version": "1.0.0", + "description": "Système de logging centralisé avec traçage hiérarchique et visualisation temps réel", + "main": "ErrorReporting.js", + "scripts": { + "logs": "node logviewer.cjs", + "logs:pretty": "node logviewer.cjs --pretty", + "logs:search": "node logviewer.cjs --search --pretty", + "logs:errors": "node logviewer.cjs --level ERROR --pretty", + "logs:server": "node log-server.cjs", + "logs:viewer": "node log-server.cjs && start logs-viewer.html" + }, + "dependencies": { + "ws": "^8.14.0", + "pino": "^8.15.0", + "pino-pretty": "^10.2.0" + }, + "devDependencies": {}, + "keywords": [ + "logging", + "tracing", + "websocket", + "real-time", + "json-logs", + "cli-tools" + ], + "author": "SEO Generator Team", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "ErrorReporting.js", + "trace.js", + "trace-wrap.js", + "logviewer.cjs", + "logs-viewer.html", + "log-server.cjs", + "README.md" + ] +} \ No newline at end of file diff --git a/export_logger/trace-wrap.js b/export_logger/trace-wrap.js new file mode 100644 index 0000000..c4a8526 --- /dev/null +++ b/export_logger/trace-wrap.js @@ -0,0 +1,9 @@ +// lib/trace-wrap.js +const { tracer } = require('./trace.js'); + +const traced = (name, fn, attrs) => (...args) => + tracer.run(name, () => fn(...args), attrs); + +module.exports = { + traced +}; \ No newline at end of file diff --git a/export_logger/trace.js b/export_logger/trace.js new file mode 100644 index 0000000..6b1ce1b --- /dev/null +++ b/export_logger/trace.js @@ -0,0 +1,156 @@ +// lib/trace.js +const { AsyncLocalStorage } = require('node:async_hooks'); +const { randomUUID } = require('node:crypto'); +const { logSh } = require('./ErrorReporting'); + +const als = new AsyncLocalStorage(); + +function now() { return performance.now(); } +function dur(ms) { + if (ms < 1e3) return `${ms.toFixed(1)}ms`; + const s = ms / 1e3; + return s < 60 ? `${s.toFixed(2)}s` : `${(s/60).toFixed(2)}m`; +} + +class Span { + constructor({ name, parent = null, attrs = {} }) { + this.id = randomUUID(); + this.name = name; + this.parent = parent; + this.children = []; + this.attrs = attrs; + this.start = now(); + this.end = null; + this.status = 'ok'; + this.error = null; + } + pathNames() { + const names = []; + let cur = this; + while (cur) { names.unshift(cur.name); cur = cur.parent; } + return names.join(' > '); + } + finish() { this.end = now(); } + duration() { return (this.end ?? now()) - this.start; } +} + +class Tracer { + constructor() { + this.rootSpans = []; + } + current() { return als.getStore(); } + + async startSpan(name, attrs = {}) { + const parent = this.current(); + const span = new Span({ name, parent, attrs }); + if (parent) parent.children.push(span); + else this.rootSpans.push(span); + + // Formater les paramètres pour affichage + const paramsStr = this.formatParams(attrs); + await logSh(`▶ ${name}${paramsStr}`, 'TRACE'); + return span; + } + + async run(name, fn, attrs = {}) { + const parent = this.current(); + const span = await this.startSpan(name, attrs); + return await als.run(span, async () => { + try { + const res = await fn(); + span.finish(); + const paramsStr = this.formatParams(span.attrs); + await logSh(`✔ ${name}${paramsStr} (${dur(span.duration())})`, 'TRACE'); + return res; + } catch (err) { + span.status = 'error'; + span.error = { message: err?.message, stack: err?.stack }; + span.finish(); + const paramsStr = this.formatParams(span.attrs); + await logSh(`✖ ${name}${paramsStr} FAILED (${dur(span.duration())})`, 'ERROR'); + await logSh(`Stack trace: ${span.error.message}`, 'ERROR'); + if (span.error.stack) { + const stackLines = span.error.stack.split('\n').slice(1, 6); // Première 5 lignes du stack + for (const line of stackLines) { + await logSh(` ${line.trim()}`, 'ERROR'); + } + } + throw err; + } + }); + } + + async event(msg, extra = {}) { + const span = this.current(); + const data = { trace: true, evt: 'span.event', ...extra }; + if (span) { + data.span = span.id; + data.path = span.pathNames(); + data.since_ms = +( (now() - span.start).toFixed(1) ); + } + await logSh(`• ${msg}`, 'TRACE'); + } + + async annotate(fields = {}) { + const span = this.current(); + if (span) Object.assign(span.attrs, fields); + await logSh('… annotate', 'TRACE'); + } + + formatParams(attrs = {}) { + const params = Object.entries(attrs) + .filter(([key, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + // Tronquer les valeurs trop longues + const strValue = String(value); + const truncated = strValue.length > 50 ? strValue.substring(0, 47) + '...' : strValue; + return `${key}=${truncated}`; + }); + + return params.length > 0 ? `(${params.join(', ')})` : ''; + } + + printSummary() { + const lines = []; + const draw = (node, depth = 0) => { + const pad = ' '.repeat(depth); + const icon = node.status === 'error' ? '✖' : '✔'; + lines.push(`${pad}${icon} ${node.name} (${dur(node.duration())})`); + if (Object.keys(node.attrs ?? {}).length) { + lines.push(`${pad} attrs: ${JSON.stringify(node.attrs)}`); + } + for (const ch of node.children) draw(ch, depth + 1); + if (node.status === 'error' && node.error?.message) { + lines.push(`${pad} error: ${node.error.message}`); + if (node.error.stack) { + const stackLines = String(node.error.stack || '').split('\n').slice(1, 4).map(s => s.trim()); + if (stackLines.length) { + lines.push(`${pad} stack:`); + stackLines.forEach(line => { + if (line) lines.push(`${pad} ${line}`); + }); + } + } + } + }; + for (const r of this.rootSpans) draw(r, 0); + const summary = lines.join('\n'); + logSh(`\n—— TRACE SUMMARY ——\n${summary}\n—— END TRACE ——`, 'INFO'); + return summary; + } +} + +const tracer = new Tracer(); + +function setupTracer(moduleName = 'Default') { + return { + run: (name, fn, params = {}) => tracer.run(name, fn, params) + }; +} + +module.exports = { + Span, + Tracer, + tracer, + setupTracer +}; \ No newline at end of file diff --git a/export_logger/websocket-server.js b/export_logger/websocket-server.js new file mode 100644 index 0000000..b5b447d --- /dev/null +++ b/export_logger/websocket-server.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Serveur WebSocket simple pour recevoir les logs temps réel + +const WebSocket = require('ws'); +const port = 8082; + +// Créer le serveur WebSocket +const wss = new WebSocket.Server({ port: port }); + +console.log(`🚀 Serveur WebSocket démarré sur le port ${port}`); +console.log(`📡 En attente de connexions...`); + +// Garder trace des clients connectés +const clients = new Set(); + +wss.on('connection', function connection(ws) { + console.log('✅ 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); + console.log('📨 Message reçu:', message); + // DIFFUSER LE LOG À TOUS LES CLIENTS CONNECTÉS ! + broadcastLog(message); + } catch (error) { + console.log('📨 Message reçu (brut):', data.toString()); + } + }); + + // Nettoyer quand le client se déconnecte + ws.on('close', function close() { + console.log('❌ Client déconnecté'); + clients.delete(ws); + }); + + // Gérer les erreurs + ws.on('error', function error(err) { + console.log('❌ 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) { + console.log('❌ Erreur envoi vers client:', error.message); + clients.delete(ws); + } + } else { + // Nettoyer les connexions fermées + clients.delete(ws); + } + }); + + if (sentCount > 0) { + console.log(`📡 Log diffusé à ${sentCount} client(s): [${logData.level}] ${logData.message.substring(0, 50)}${logData.message.length > 50 ? '...' : ''}`); + } +} + +// 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', () => { + console.log('\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(() => { + console.log('✅ Serveur WebSocket arrêté'); + 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); \ No newline at end of file diff --git a/index.html b/index.html index 046efa7..2826e3e 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,16 @@ + +
+
🎓 Cours d'Anglais Interactif
+
+
+ Connexion... +
+ +
+