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
+
+
+
+
+
+
+
+
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
+
+
+
+