Fix WebSocket logging system and add comprehensive network features

- Fix WebSocket server to properly broadcast logs to all connected clients
- Integrate professional logging system with real-time WebSocket interface
- Add network status indicator with DigitalOcean Spaces connectivity
- Implement AWS Signature V4 authentication for private bucket access
- Add JSON content loader with backward compatibility to JS modules
- Restore navigation breadcrumb system with comprehensive logging
- Add multiple content formats: JSON + JS with automatic discovery
- Enhance top bar with logger toggle and network status indicator
- Remove deprecated temp-games module and clean up unused files

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-09-15 23:05:14 +08:00
parent 855cfe2f94
commit 1f8688c4aa
51 changed files with 6473 additions and 1521 deletions

View File

@ -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)
@ -328,3 +379,6 @@ Open `index.html` in a web browser - no build process required. All modules load
- Touch-friendly interface
- Portrait/landscape orientation support
- 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

View File

@ -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)

View File

@ -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"]

View File

@ -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,7 +501,19 @@ 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 {

View File

@ -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 !** 🎉

View File

@ -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 = `
<div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
<h1 style="color: ${statusColor};">Rapport Workflow SEO Automatisé (Node.js)</h1>
<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Résumé Exécutif</h2>
<p><strong>Statut:</strong> <span style="color: ${statusColor};">${report.status}</span></p>
<p><strong>Article:</strong> ${report.csvData.t0}</p>
<p><strong>Mot-clé:</strong> ${report.csvData.mc0}</p>
<p><strong>Taux de réussite:</strong> ${report.stats.successRate}%</p>
<p><strong>Timestamp:</strong> ${report.timestamp}</p>
<p><strong>Plateforme:</strong> Node.js Server</p>
</div>`;
if (report.errors.length > 0) {
html += `<div style="background: #f8d7da; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Erreurs Critiques (${report.errors.length})</h2>`;
report.errors.forEach((error, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #dc3545;">
<h4>${i + 1}. ${error.type}</h4>
<p><strong>Message:</strong> ${error.message}</p>
<p><strong>Impact:</strong> ${error.impact}</p>
${error.suggestion ? `<p><strong>Solution:</strong> ${error.suggestion}</p>` : ''}
</div>`;
});
html += `</div>`;
}
if (report.warnings.length > 0) {
html += `<div style="background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Avertissements (${report.warnings.length})</h2>`;
report.warnings.forEach((warning, i) => {
html += `
<div style="margin: 10px 0; padding: 10px; border-left: 3px solid #ffc107;">
<h4>${i + 1}. ${warning.type}</h4>
<p>${warning.message}</p>
</div>`;
});
html += `</div>`;
}
html += `
<div style="background: #e9ecef; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Statistiques Détaillées</h2>
<ul>
<li>Éléments extraits: ${report.stats.elementsExtracted}</li>
<li>Contenus générés: ${report.stats.contentGenerated}</li>
<li>Tags remplacés: ${report.stats.tagsReplaced}</li>
<li>Tags restants: ${report.stats.tagsRemaining}</li>
</ul>
</div>
<div style="background: #d1ecf1; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2>Informations Système</h2>
<ul>
<li>Plateforme: Node.js</li>
<li>Version: ${process.version}</li>
<li>Mémoire: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB</li>
<li>Uptime: ${Math.round(process.uptime())}s</li>
</ul>
</div>
</div>`;
return html;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
logSh,
setupTracer: require('./trace').setupTracer,
cleanLogSheet,
validateWorkflowIntegrity,
detectDuplicateTags,
detectMissingCSVVariables,
assessGenerationQuality,
sendErrorReport,
createHTMLReport,
initWebSocketServer
};

310
export_logger/README.md Normal file
View File

@ -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 ! 🎯

203
export_logger/demo.js Normal file
View File

@ -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 };

View File

@ -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(`
<!DOCTYPE html>
<html>
<head>
<title>Log Viewer Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
h1 { color: #333; }
.log-list { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.log-item {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.log-item:hover { background: #f8f9fa; }
.log-name { font-weight: bold; color: #2c5aa0; }
.log-info { font-size: 0.9em; color: #666; }
.view-btn {
background: #007bff;
color: white;
padding: 5px 15px;
text-decoration: none;
border-radius: 3px;
font-size: 0.9em;
}
.view-btn:hover { background: #0056b3; }
.realtime-btn {
background: #28a745;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
display: inline-block;
margin-bottom: 20px;
}
.realtime-btn:hover { background: #218838; }
</style>
</head>
<body>
<h1>📊 SEO Generator - Log Viewer</h1>
<a href="/tools/logs-viewer.html" class="realtime-btn">🔴 Logs en temps réel</a>
<div class="log-list">
<h2>Fichiers de log disponibles</h2>
<div id="logFiles">Chargement...</div>
</div>
<script>
async function loadLogFiles() {
try {
const response = await fetch('/api/logs');
const data = await response.json();
const container = document.getElementById('logFiles');
if (data.files.length === 0) {
container.innerHTML = '<p>Aucun fichier de log trouvé</p>';
return;
}
container.innerHTML = data.files.map(file => {
const sizeKB = Math.round(file.size / 1024);
const date = new Date(file.modified).toLocaleString('fr-FR');
return \`
<div class="log-item">
<div>
<div class="log-name">\${file.name}</div>
<div class="log-info">\${sizeKB} KB \${date}</div>
</div>
<a href="\${file.url}" class="view-btn" target="_blank">Voir</a>
</div>
\`;
}).join('');
} catch (error) {
document.getElementById('logFiles').innerHTML =
'<p style="color: red;">Erreur: ' + error.message + '</p>';
}
}
loadLogFiles();
</script>
</body>
</html>
`);
});
// 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);
});

View File

@ -0,0 +1,928 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEO Generator - Logs en temps réel</title>
<style>
body {
font-family: 'Courier New', monospace;
background: #1e1e1e;
color: #ffffff;
margin: 0;
padding: 4px;
}
.header {
background: #2d2d30;
padding: 4px;
border-radius: 2px;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
margin: 0;
font-size: 12px;
}
.header-right {
display: flex;
gap: 4px;
align-items: center;
}
.status {
display: inline-block;
padding: 2px 4px;
border-radius: 1px;
font-size: 9px;
font-weight: bold;
}
.status.connected { background: #28a745; }
.status.disconnected { background: #dc3545; }
.status.connecting { background: #ffc107; color: #000; }
.logs-container {
height: calc(100vh - 88px);
overflow-y: auto;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 2px;
padding: 4px;
}
.log-entry {
padding: 2px 0;
border-bottom: 1px solid #21262d;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.log-entry.unwrapped {
white-space: pre-wrap;
overflow: visible;
text-overflow: unset;
background: rgba(88, 166, 255, 0.05);
border-left: 2px solid #58a6ff;
padding-left: 4px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.trace {
background: rgba(31, 111, 235, 0.1);
padding-left: 1px;
border-left: 2px solid #1f6feb;
}
.log-entry.trace.span-start {
border-left-color: #28a745;
}
.log-entry.trace.span-end {
border-left-color: #17a2b8;
}
.log-entry.trace.span-error {
border-left-color: #dc3545;
background: rgba(220, 53, 69, 0.1);
}
.log-entry.stack-trace {
background: rgba(220, 53, 69, 0.05);
padding-left: 1px;
color: #f85149;
font-family: 'Courier New', monospace;
font-size: 10px;
border-left: 2px solid #dc3545;
}
.log-details {
margin-top: 4px;
padding: 4px;
background: rgba(139, 148, 158, 0.1);
border-radius: 2px;
font-size: 9px;
color: #8b949e;
display: none;
}
.show-details .log-details {
display: block;
}
.details-toggle {
background: none;
color: #58a6ff;
border: 1px solid #58a6ff;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.details-toggle:hover {
background: rgba(88, 166, 255, 0.1);
}
.unwrap-toggle {
background: none;
color: #f79009;
border: 1px solid #f79009;
padding: 1px 1px;
font-size: 8px;
margin-right: 4px;
}
.unwrap-toggle:hover {
background: rgba(247, 144, 9, 0.1);
}
.search-container {
margin-bottom: 3px;
display: flex;
gap: 4px;
align-items: center;
}
.search-input {
flex-grow: 1;
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 4px 6px;
border-radius: 2px;
font-size: 11px;
}
.search-input:focus {
outline: none;
border-color: #58a6ff;
background: #0d1117;
}
.search-info {
color: #7d8590;
font-size: 10px;
min-width: 80px;
}
.log-entry.search-match {
background: rgba(255, 193, 7, 0.2);
border-left: 3px solid #ffc107;
}
.log-entry.search-current {
background: rgba(255, 193, 7, 0.4);
border-left: 3px solid #ffc107;
}
.search-highlight {
background: #ffc107;
color: #000;
padding: 1px 2px;
border-radius: 2px;
}
.timestamp {
color: #7d8590;
margin-right: 1px;
font-size: 11px;
}
.level {
font-weight: bold;
margin-right: 1px;
padding: 1px 1px;
border-radius: 2px;
font-size: 11px;
min-width: 32px;
}
.level.INFO { background: #1f6feb; }
.level.WARN, .level.WARNING { background: #d29922; }
.level.ERROR { background: #da3633; }
.level.DEBUG { background: #8b949e; }
.level.TRACE { background: #238636; }
.level.PROMPT { background: #8b5cf6; }
.level.LLM { background: #f97316; }
button {
background: #238636;
color: white;
border: none;
padding: 3px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
button:hover { background: #2ea043; }
button:disabled { background: #6e7781; cursor: not-allowed; }
.filter-toggles {
display: flex;
gap: 2px;
align-items: center;
margin-left: 6px;
}
.filter-toggle {
background: #21262d;
border: 1px solid #30363d;
color: #f0f6fc;
padding: 2px 4px;
border-radius: 1px;
cursor: pointer;
font-size: 9px;
min-width: 40px;
text-align: center;
}
.filter-toggle.active.trace { background: #238636; border-color: #238636; }
.filter-toggle.active.info { background: #1f6feb; border-color: #1f6feb; }
.filter-toggle.active.debug { background: #8b949e; border-color: #8b949e; }
.filter-toggle.active.warn { background: #d29922; border-color: #d29922; }
.filter-toggle.active.error { background: #da3633; border-color: #da3633; }
.filter-toggle.active.prompt { background: #8b5cf6; border-color: #8b5cf6; }
.filter-toggle:hover { background: #30363d; }
.log-entry.hidden-by-filter { display: none !important; }
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1>SEO Generator - Logs temps réel</h1>
<span id="status" class="status connecting">Connexion...</span>
<span style="margin-left: 15px; font-size: 12px;">Port: <strong>8082</strong></span>
<br>
<button onclick="toggleGlobalDetails()" id="detailsBtn">Mode détaillé: OFF</button>
<button onclick="toggleLineUnwrap()" id="lineUnwrapBtn">Unwrap ligne: OFF</button>
</div>
<div class="header-right">
<div class="filter-toggles">
<span style="color: #7d8590; font-size: 11px;">Filtres:</span>
<button class="filter-toggle active trace" onclick="toggleLevelFilter('trace')" id="traceFilter">TRACE</button>
<button class="filter-toggle active info" onclick="toggleLevelFilter('info')" id="infoFilter">INFO</button>
<button class="filter-toggle active debug" onclick="toggleLevelFilter('debug')" id="debugFilter">DEBUG</button>
<button class="filter-toggle active warn" onclick="toggleLevelFilter('warn')" id="warnFilter">WARN</button>
<button class="filter-toggle active error" onclick="toggleLevelFilter('error')" id="errorFilter">ERROR</button>
<button class="filter-toggle active prompt" onclick="toggleLevelFilter('prompt')" id="promptFilter">PROMPT</button>
<button class="filter-toggle active llm" onclick="toggleLevelFilter('llm')" id="llmFilter">LLM</button>
</div>
<button onclick="clearLogs()">Effacer</button>
<button onclick="toggleAutoScroll()" id="autoScrollBtn">Auto-scroll: ON</button>
<button onclick="reconnect()" id="reconnectBtn">Reconnecter</button>
</div>
</div>
<div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Rechercher dans les logs... (Ctrl+F)">
<div class="search-info" id="searchInfo">0 résultats</div>
<button onclick="searchPrevious()" id="searchPrevBtn" disabled>⬆ Précédent</button>
<button onclick="searchNext()" id="searchNextBtn" disabled>⬇ Suivant</button>
<button onclick="clearSearch()" id="clearSearchBtn"></button>
</div>
<div class="logs-container" id="logsContainer">
<div class="log-entry">
<span class="timestamp">--:--:--</span>
<span class="level INFO">INFO</span>
En attente des logs...
</div>
</div>
<script>
let ws;
let autoScroll = true;
const logsContainer = document.getElementById('logsContainer');
const statusElement = document.getElementById('status');
// Variables de recherche
let searchMatches = [];
let currentMatchIndex = -1;
let searchTerm = '';
// Variables de filtrage
let levelFilters = {
trace: true,
info: true,
debug: true,
warn: true,
warning: true,
error: true,
prompt: true,
llm: true
};
// Récupérer le fichier de log depuis l'URL
const urlParams = new URLSearchParams(window.location.search);
const logFile = urlParams.get('file');
console.log('🌐 URL params:', window.location.search, 'logFile:', logFile);
if (logFile) {
// Mode fichier : charger le fichier spécifié
console.log('📁 MODE FICHIER activé pour:', logFile);
document.title = `SEO Generator - Logs: ${logFile}`;
document.querySelector('h1').textContent = `Logs: ${logFile}`;
loadLogFile(logFile);
} else {
// Mode temps réel : WebSocket comme avant
console.log('⚡ MODE WEBSOCKET activé - pas de paramètre file');
connect();
}
async function loadLogFile(filename) {
try {
statusElement.textContent = `Chargement ${filename}...`;
statusElement.className = 'status connecting';
// Utiliser file:// pour lire directement le fichier local
const input = document.createElement('input');
input.type = 'file';
input.accept = '.log';
input.style.display = 'none';
input.onchange = function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const logContent = e.target.result;
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
// Parser et afficher chaque ligne
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
// Ligne non-JSON, afficher telle quelle
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
};
reader.readAsText(file);
};
// Si un nom de fichier est spécifié, tenter de le charger depuis logs/
if (filename) {
try {
const response = await fetch(`logs/${filename}`);
if (response.ok) {
const logContent = await response.text();
const lines = logContent.split('\n').filter(line => line.trim());
statusElement.textContent = `Fichier chargé (${lines.length} lignes)`;
statusElement.className = 'status connected';
lines.forEach(line => {
try {
const logData = JSON.parse(line);
const timestamp = new Date(logData.time).toISOString();
const level = normalizeLevelName(logData.level);
addLogEntry(logData.msg || logData.message || line, level, timestamp, line);
} catch (error) {
addLogEntry(line, 'INFO', new Date().toISOString(), line);
}
});
return;
}
} catch (fetchError) {
// Si le fetch échoue, demander à l'utilisateur de sélectionner le fichier
}
}
// Demander à l'utilisateur de sélectionner le fichier
addLogEntry(`Sélectionnez le fichier de log ${filename || ''} à charger`, 'INFO');
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
} catch (error) {
statusElement.textContent = `Erreur: ${error.message}`;
statusElement.className = 'status disconnected';
addLogEntry(`Erreur chargement fichier: ${error.message}`, 'ERROR');
}
}
function normalizeLevelName(level) {
const levelMap = {10:'TRACE',20:'DEBUG',25:'PROMPT',26:'LLM',30:'INFO',40:'WARN',50:'ERROR',60:'FATAL'};
if (typeof level === 'number') {
return levelMap[level] || 'INFO';
}
return String(level).toUpperCase();
}
function connect() {
console.log('🔌 connect() appelé - tentative WebSocket ws://localhost:8082');
ws = new WebSocket('ws://localhost:8082');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusElement.textContent = 'Connecté';
statusElement.className = 'status connected';
// Reset des tentatives de reconnexion
reconnectAttempts = 0;
reconnectDelay = 1000; // Reconnexion ultra rapide
// NE PAS vider les logs existants - garder l'historique
// logsContainer.innerHTML = '';
addLogEntry('🔗 WebSocket connecté - Réception des logs temps réel...', 'INFO');
};
ws.onmessage = (event) => {
console.log('📨 Message WebSocket reçu:', event.data);
try {
const logData = JSON.parse(event.data);
console.log('📊 Log parsé:', logData.level, '→', logData.message.substring(0, 50));
addLogEntry(logData.message, logData.level, logData.timestamp, event.data);
} catch (error) {
console.log('❌ Erreur parsing:', error);
addLogEntry('Erreur parsing log: ' + event.data, 'ERROR');
}
};
ws.onclose = () => {
statusElement.textContent = 'Déconnecté';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
ws.onerror = (error) => {
statusElement.textContent = 'Erreur';
statusElement.className = 'status disconnected';
// Auto-reconnexion immédiate
scheduleReconnect();
};
}
let showDetailsMode = false;
function addLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const time = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
// Déterminer si c'est une trace et son type
let traceClass = '';
let cleanMessage = message;
if (message.includes('▶')) {
traceClass = 'trace span-start';
// Nettoyer le message pour garder uniquement l'info utile
cleanMessage = message.replace('▶ ', '🔵 ');
} else if (message.includes('✔')) {
traceClass = 'trace span-end';
cleanMessage = message.replace('✔ ', '✅ ');
} else if (message.includes('✖')) {
traceClass = 'trace span-error';
cleanMessage = message.replace('✖ ', '❌ ');
} else if (message.includes('•')) {
traceClass = 'trace';
cleanMessage = message.replace('• ', '📝 ');
} else if (message.includes('Stack trace:') || message.trim().startsWith('at ')) {
traceClass = 'stack-trace';
if (message.includes('Stack trace:')) {
cleanMessage = '🔴 ' + message;
} else {
cleanMessage = ' ' + message; // Indentation pour les lignes de stack
}
}
logEntry.className += ' ' + traceClass;
const hasDetails = rawData && rawData !== JSON.stringify({message, level, timestamp});
const detailsButton = hasDetails ?
`<button class="details-toggle" onclick="toggleDetails(this)">détails</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
// Détecter si le message est trop long (approximation simple)
const isMessageTooLong = cleanMessage.length > 80;
const unwrapButton = isMessageTooLong ?
`<button class="unwrap-toggle" onclick="toggleUnwrap(this)">unwrap</button>` :
`<span style="display: inline-block; width: 41px;"></span>`; // Placeholder pour alignement
logEntry.innerHTML = `
${detailsButton}
${unwrapButton}
<span class="timestamp">${time}</span>
<span class="level ${level}">${level}</span>
${cleanMessage}
${hasDetails ? `<div class="log-details"><pre>${JSON.stringify(JSON.parse(rawData), null, 2)}</pre></div>` : ''}
`;
// Appliquer le mode détails global si activé
if (showDetailsMode && hasDetails) {
logEntry.classList.add('show-details');
}
// Appliquer les filtres de niveau
applyLevelFilterToEntry(logEntry, level);
// Ajouter le click listener pour l'unwrap ligne par ligne
logEntry.addEventListener('click', (e) => {
// Ne pas déclencher si on clique sur un bouton
if (e.target.classList.contains('details-toggle') ||
e.target.classList.contains('unwrap-toggle')) return;
toggleLogEntryWrap(logEntry);
});
logsContainer.appendChild(logEntry);
// Auto-scroll intelligent : seulement si l'utilisateur est déjà en bas
if (autoScroll) {
// Détection plus précise : considérer qu'on est "en bas" si on est à moins de 100px du bas
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// Scroll immédiat vers le bas
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
}
function toggleDetails(button) {
const logEntry = button.parentElement;
logEntry.classList.toggle('show-details');
button.textContent = logEntry.classList.contains('show-details') ? 'masquer' : 'détails';
}
function toggleUnwrap(button) {
const logEntry = button.parentElement;
if (logEntry.classList.contains('unwrapped')) {
// Remettre en mode wrapped
logEntry.classList.remove('unwrapped');
logEntry.style.whiteSpace = 'nowrap';
logEntry.style.overflow = 'hidden';
logEntry.style.textOverflow = 'ellipsis';
button.textContent = 'unwrap';
} else {
// Passer en mode unwrapped
logEntry.classList.add('unwrapped');
logEntry.style.whiteSpace = 'pre-wrap';
logEntry.style.overflow = 'visible';
logEntry.style.textOverflow = 'unset';
button.textContent = 'wrap';
}
}
function toggleGlobalDetails() {
showDetailsMode = !showDetailsMode;
const detailsBtn = document.getElementById('detailsBtn');
detailsBtn.textContent = `Mode détaillé: ${showDetailsMode ? 'ON' : 'OFF'}`;
// Appliquer/retirer le mode détails à toutes les entrées
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
if (showDetailsMode) {
entry.classList.add('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'masquer';
} else {
entry.classList.remove('show-details');
const toggle = entry.querySelector('.details-toggle');
if (toggle) toggle.textContent = 'détails';
}
});
}
function clearLogs() {
logsContainer.innerHTML = '';
addLogEntry('Logs effacés', 'INFO');
}
function toggleAutoScroll() {
autoScroll = !autoScroll;
document.getElementById('autoScrollBtn').textContent = `Auto-scroll: ${autoScroll ? 'ON' : 'OFF'}`;
}
// Variables pour le unwrap ligne par ligne
let lineUnwrapMode = false;
function toggleLineUnwrap() {
lineUnwrapMode = !lineUnwrapMode;
document.getElementById('lineUnwrapBtn').textContent = `Unwrap ligne: ${lineUnwrapMode ? 'ON' : 'OFF'}`;
if (!lineUnwrapMode) {
// Désactiver le mode : remettre toutes les lignes en mode compact
const logEntries = document.querySelectorAll('.log-entry');
logEntries.forEach(entry => {
entry.classList.remove('unwrapped');
});
}
}
// Fonction pour unwrap/wrap une ligne individuelle
function toggleLogEntryWrap(logEntry) {
if (!lineUnwrapMode) return; // Mode désactivé
if (logEntry.classList.contains('unwrapped')) {
// Re-wrapper la ligne
logEntry.classList.remove('unwrapped');
} else {
// Unwrapper la ligne
logEntry.classList.add('unwrapped');
}
}
function reconnect() {
if (ws) {
ws.close();
}
statusElement.textContent = 'Reconnexion...';
statusElement.className = 'status connecting';
setTimeout(connect, 1000);
}
// Fonctions de recherche
function performSearch() {
const searchInput = document.getElementById('searchInput');
const searchInfo = document.getElementById('searchInfo');
const searchPrevBtn = document.getElementById('searchPrevBtn');
const searchNextBtn = document.getElementById('searchNextBtn');
searchTerm = searchInput.value.trim().toLowerCase();
// Effacer les recherches précédentes
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
if (searchTerm === '') {
searchInfo.textContent = '0 résultats';
searchPrevBtn.disabled = true;
searchNextBtn.disabled = true;
return;
}
// Rechercher dans tous les logs visibles
const logEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
logEntries.forEach((entry, index) => {
const text = entry.textContent.toLowerCase();
if (text.includes(searchTerm)) {
searchMatches.push(entry);
entry.classList.add('search-match');
// Highlighter le texte
highlightTextInElement(entry, searchTerm);
}
});
// Mettre à jour l'interface
searchInfo.textContent = `${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
searchPrevBtn.disabled = searchMatches.length === 0;
searchNextBtn.disabled = searchMatches.length === 0;
// Aller au premier résultat
if (searchMatches.length > 0) {
currentMatchIndex = 0;
scrollToCurrentMatch();
}
}
function highlightTextInElement(element, term) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
if (node.textContent.toLowerCase().includes(term)) {
textNodes.push(node);
}
}
textNodes.forEach(textNode => {
const parent = textNode.parentNode;
const text = textNode.textContent;
const lowerText = text.toLowerCase();
const regex = new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
if (lowerText.includes(term)) {
const highlightedHTML = text.replace(regex, '<span class="search-highlight">$1</span>');
const wrapper = document.createElement('span');
wrapper.innerHTML = highlightedHTML;
parent.insertBefore(wrapper, textNode);
parent.removeChild(textNode);
}
});
}
function clearSearchHighlights() {
const highlights = document.querySelectorAll('.search-highlight');
highlights.forEach(highlight => {
const parent = highlight.parentNode;
parent.replaceChild(document.createTextNode(highlight.textContent), highlight);
parent.normalize();
});
const searchMatches = document.querySelectorAll('.search-match, .search-current');
searchMatches.forEach(match => {
match.classList.remove('search-match', 'search-current');
});
}
function scrollToCurrentMatch() {
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
// Retirer la classe current de l'ancien match
searchMatches.forEach(match => match.classList.remove('search-current'));
// Ajouter la classe current au match actuel
const currentMatch = searchMatches[currentMatchIndex];
currentMatch.classList.add('search-current');
// Scroller vers l'élément
currentMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Mettre à jour l'info de recherche
document.getElementById('searchInfo').textContent =
`${currentMatchIndex + 1}/${searchMatches.length} résultat${searchMatches.length > 1 ? 's' : ''}`;
}
}
function searchNext() {
if (searchMatches.length > 0) {
currentMatchIndex = (currentMatchIndex + 1) % searchMatches.length;
scrollToCurrentMatch();
}
}
function searchPrevious() {
if (searchMatches.length > 0) {
currentMatchIndex = currentMatchIndex === 0 ? searchMatches.length - 1 : currentMatchIndex - 1;
scrollToCurrentMatch();
}
}
function clearSearch() {
document.getElementById('searchInput').value = '';
clearSearchHighlights();
searchMatches = [];
currentMatchIndex = -1;
document.getElementById('searchInfo').textContent = '0 résultats';
document.getElementById('searchPrevBtn').disabled = true;
document.getElementById('searchNextBtn').disabled = true;
}
// Event listeners pour la recherche
document.getElementById('searchInput').addEventListener('input', performSearch);
document.getElementById('searchInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
if (e.shiftKey) {
searchPrevious();
} else {
searchNext();
}
} else if (e.key === 'Escape') {
clearSearch();
}
});
// Fonctions de filtrage par niveau
function applyLevelFilterToEntry(entry, level) {
const normalizedLevel = level.toLowerCase();
console.log(`🔍 Filtre niveau: "${level}" → "${normalizedLevel}" → filtre actif: ${!!levelFilters[normalizedLevel]}`);
if (!levelFilters[normalizedLevel]) {
entry.classList.add('hidden-by-filter');
console.log(`❌ Log caché: ${normalizedLevel}`);
} else {
entry.classList.remove('hidden-by-filter');
console.log(`✅ Log visible: ${normalizedLevel}`);
}
}
function toggleLevelFilter(level) {
levelFilters[level] = !levelFilters[level];
levelFilters['warning'] = levelFilters['warn']; // Synchroniser warn/warning
const button = document.getElementById(`${level}Filter`);
if (levelFilters[level]) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
// Capturer le pourcentage de position AVANT d'appliquer le filtre
const currentScroll = logsContainer.scrollTop;
const maxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const currentViewPercentage = maxScroll > 0 ? currentScroll / maxScroll : 0;
// Appliquer les filtres à tous les logs
const entries = document.querySelectorAll('.log-entry');
entries.forEach(entry => {
const entryLevel = entry.querySelector('.level').textContent.toLowerCase();
applyLevelFilterToEntry(entry, entryLevel);
});
// Re-effectuer la recherche si active
if (searchTerm) {
performSearch();
}
// Scroll intelligent avec le pourcentage capturé
smartScrollAfterFilter(currentViewPercentage);
}
function smartScrollAfterFilter(currentViewPercentage) {
setTimeout(() => {
const visibleEntries = document.querySelectorAll('.log-entry:not(.hidden-by-filter)');
if (visibleEntries.length === 0) return;
// Si on a un match de recherche actuel, privilégier celui-ci
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
const currentSearchMatch = searchMatches[currentMatchIndex];
if (!currentSearchMatch.classList.contains('hidden-by-filter')) {
currentSearchMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
}
// Appliquer le même pourcentage aux nouvelles entrées visibles
// Attendre que le DOM se mette à jour après l'application des filtres
setTimeout(() => {
const newMaxScroll = logsContainer.scrollHeight - logsContainer.clientHeight;
const targetScroll = newMaxScroll * currentViewPercentage;
logsContainer.scrollTo({
top: Math.max(0, Math.min(targetScroll, newMaxScroll)),
behavior: 'smooth'
});
}, 50);
}, 100);
}
// Raccourci Ctrl+F
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.getElementById('searchInput').focus();
}
});
// Connexion initiale SEULEMENT si pas en mode fichier
// (connect() est déjà appelé dans la logique if/else plus haut)
// Auto-reconnexion intelligente
let reconnectDelay = 1000; // 1 seconde
let reconnectAttempts = 0;
let maxReconnectAttempts = 50; // Limite raisonnable
function scheduleReconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
addLogEntry('Nombre max de tentatives de reconnexion atteint', 'ERROR');
return;
}
setTimeout(() => {
if (!ws || ws.readyState === WebSocket.CLOSED) {
reconnectAttempts++;
statusElement.textContent = `Reconnexion... (${reconnectAttempts}/${maxReconnectAttempts})`;
statusElement.className = 'status connecting';
connect();
}
}, reconnectDelay);
}
// Gestion intelligente de l'auto-scroll basée sur le comportement utilisateur
let userScrolledAway = false;
let scrollTimeout;
logsContainer.addEventListener('scroll', () => {
if (!autoScroll) return;
clearTimeout(scrollTimeout);
const scrollTop = logsContainer.scrollTop;
const scrollHeight = logsContainer.scrollHeight;
const clientHeight = logsContainer.clientHeight;
const isAtBottom = (scrollTop + clientHeight) >= (scrollHeight - 100);
if (isAtBottom) {
// L'utilisateur est revenu en bas, réactiver l'auto-scroll
if (userScrolledAway) {
userScrolledAway = false;
console.log('🔄 Auto-scroll réactivé - utilisateur revenu en bas');
}
} else {
// L'utilisateur a scrollé vers le haut, marquer qu'il s'est éloigné du bas
userScrolledAway = true;
}
// Debounce pour éviter trop d'événements
scrollTimeout = setTimeout(() => {
// Logique supplémentaire si nécessaire
}, 150);
});
// Améliorer addLogEntry pour respecter userScrolledAway
const originalAddLogEntry = addLogEntry;
function enhancedAddLogEntry(message, level = 'INFO', timestamp = null, rawData = null) {
originalAddLogEntry(message, level, timestamp, rawData);
// Override : si l'utilisateur n'a pas scrollé manuellement ET que l'auto-scroll est ON,
// forcer le scroll vers le bas
if (autoScroll && !userScrolledAway) {
requestAnimationFrame(() => {
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
}
// Remplacer la fonction globale
addLogEntry = enhancedAddLogEntry;
</script>
</body>
</html>

338
export_logger/logviewer.cjs Normal file
View File

@ -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<approxLines){
const sz=Math.min(chunk,pos); pos-=sz;
const buf=Buffer.alloc(sz); await fd.read(buf,0,sz,pos);
buffer = buf.toString('utf8') + buffer;
let parts=buffer.split(/\r?\n/); buffer=parts.shift();
for(const p of parts){ if(!p.trim()) continue; const o=safeParse(p); if(o) lines.push(o); }
}
if (buffer && buffer.trim()){ const o=safeParse(buffer); if(o) lines.unshift(o); }
return lines.slice(-approxLines);
} finally { await fd.close(); }
}
async function streamFilter(file, filters, limit){
const rl=readline.createInterface({ input: fs.createReadStream(file,{encoding:'utf8'}), crlfDelay:Infinity });
const out=[];
for await (const line of rl){
if (!line.trim()) continue;
const o=safeParse(line); if(!o) continue;
if (passesAll(o,filters)){ out.push(o); if (out.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<argv.length;i++){
const a=argv[i], nx=()=> (i+1<argv.length?argv[i+1]:undefined);
switch(a){
case '--help': case '-h': o.help=true; break;
case '--file': o.file=nx(); i++; break;
case '--last': o.last=nx(); i++; break;
case '--search': o.search=true; break;
case '--limit': o.limit=nx(); i++; break;
case '--level': o.level=nx(); i++; break;
case '--module': o.module=nx(); i++; break;
case '--since': o.since=nx(); i++; break;
case '--until': o.until=nx(); i++; break;
case '--includes': o.includes=nx(); i++; break;
case '--regex': o.regex=nx(); i++; break;
case '--pretty': o.pretty=true; break;
case '--stats': o.stats=true; break;
case '--by': o.by=nx(); i++; break;
// NEW: --timearea <ISO> <seconds>
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 <ISO_CENTER> <RADIUS_SECONDS> # 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 };

441
export_logger/package-lock.json generated Normal file
View File

@ -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
}
}
}
}
}

View File

@ -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"
]
}

View File

@ -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
};

156
export_logger/trace.js Normal file
View File

@ -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
};

View File

@ -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);

View File

@ -9,6 +9,16 @@
<link rel="stylesheet" href="css/games.css">
</head>
<body>
<!-- Top Bar with Network Status -->
<div class="top-bar">
<div class="top-bar-title">🎓 Cours d'Anglais Interactif</div>
<div class="network-status" id="network-status">
<div class="network-indicator connecting" id="network-indicator"></div>
<span class="network-status-text" id="network-status-text">Connexion...</span>
</div>
<button class="logger-toggle" onclick="openLogsInterface()" title="Ouvrir interface de logs">📋</button>
</div>
<!-- Navigation Breadcrumb -->
<nav class="breadcrumb" id="breadcrumb">
<button class="breadcrumb-item active" data-page="home">🏠 Accueil</button>
@ -19,6 +29,7 @@
<!-- Page d'Accueil -->
<div class="page active" id="home-page">
<div class="hero">
<h1>🎓 Cours d'Anglais Interactif</h1>
<p>Apprends l'anglais en t'amusant !</p>
@ -100,31 +111,181 @@
</div>
<!-- Scripts -->
<script src="js/core/websocket-logger.js"></script>
<script src="js/core/env-config.js"></script>
<script src="js/core/utils.js"></script>
<script src="js/core/content-engine.js"></script>
<script src="js/core/content-factory.js"></script>
<script src="js/core/content-parsers.js"></script>
<script src="js/core/content-generators.js"></script>
<script src="js/core/content-scanner.js"></script>
<script src="js/core/json-content-loader.js"></script>
<script src="js/tools/content-creator.js"></script>
<script src="js/core/navigation.js"></script>
<script src="js/core/game-loader.js"></script>
<script>
// Fonction de debug pour le logger
function toggleLoggerDebug() {
console.log('🔧 Bouton toggle cliqué!');
if (typeof window.logger === 'undefined') {
console.error('❌ window.logger n\'existe pas!');
alert('Erreur: Logger non initialisé!');
return;
}
console.log('✅ Logger existe, toggle...');
try {
window.logger.toggle();
console.log('✅ Toggle réussi');
} catch (error) {
console.error('❌ Erreur toggle:', error);
alert('Erreur toggle: ' + error.message);
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 DOM loaded, initializing...');
logSh('🎯 DOM chargé, initialisation de l\'application...', 'INFO');
// Vérifier que le logger existe
if (typeof window.logger === 'undefined') {
console.error('❌ Logger non trouvé au chargement!');
} else {
console.log('✅ Logger trouvé:', window.logger);
}
// Test du logger
if (typeof logSh !== 'undefined') {
logSh('🚀 Application démarrée', 'INFO');
logSh('Logger système intégré avec succès', 'DEBUG');
} else {
console.log('🚀 Application démarrée (pas de logger)');
}
// Initialize connection status listener
initConnectionStatus();
// Initialize navigation system
AppNavigation.init();
// Test initial network connection
setTimeout(testNetworkConnection, 1000);
});
// Network Status Manager
function initConnectionStatus() {
// Listen for content connection status events
window.addEventListener('contentConnectionStatus', function(event) {
updateNetworkStatus(event.detail);
});
// Test connection périodiquement
setInterval(testNetworkConnection, 30000); // Test toutes les 30 secondes
}
function updateNetworkStatus(details) {
const indicator = document.getElementById('network-indicator');
const text = document.getElementById('network-status-text');
// Remove all status classes
indicator.classList.remove('connecting', 'online', 'offline');
// Update based on status
switch(details.status) {
case 'loading':
indicator.classList.add('connecting');
text.textContent = 'Connexion...';
break;
case 'online':
indicator.classList.add('online');
text.textContent = 'En ligne';
break;
case 'offline':
indicator.classList.add('offline');
text.textContent = 'Local';
break;
case 'error':
indicator.classList.add('offline');
text.textContent = 'Hors ligne';
break;
}
}
async function testNetworkConnection() {
const indicator = document.getElementById('network-indicator');
const text = document.getElementById('network-status-text');
logSh('🔍 Test de connexion réseau démarré...', 'INFO');
try {
// Test avec l'endpoint DigitalOcean avec timeout approprié
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const testUrl = envConfig.getRemoteContentUrl() + 'test.json';
logSh(`🌐 Test URL: ${testUrl}`, 'DEBUG');
const authHeaders = await envConfig.getAuthHeaders('HEAD', testUrl);
logSh(`🔐 Headers d'auth générés: ${Object.keys(authHeaders).join(', ')}`, 'DEBUG');
const response = await fetch(testUrl, {
method: 'HEAD',
signal: controller.signal,
headers: authHeaders
});
clearTimeout(timeoutId);
logSh(`📡 Réponse reçue: ${response.status} ${response.statusText}`, 'INFO');
if (response.ok) {
indicator.classList.remove('connecting', 'offline');
indicator.classList.add('online');
text.textContent = 'En ligne';
logSh('✅ Connexion réussie !', 'INFO');
} else {
// Pour les buckets privés, on considère 403 comme "connexion OK mais accès privé"
if (response.status === 403) {
indicator.classList.remove('connecting', 'offline');
indicator.classList.add('online');
text.textContent = 'Privé';
logSh('🔒 Connexion OK mais accès privé (403)', 'WARN');
} else {
logSh(`❌ Erreur HTTP: ${response.status}`, 'ERROR');
throw new Error(`HTTP ${response.status}`);
}
}
} catch (error) {
logSh(`💥 Erreur de connexion: ${error.name} - ${error.message}`, 'ERROR');
indicator.classList.remove('connecting', 'online');
indicator.classList.add('offline');
if (error.name === 'AbortError') {
text.textContent = 'Timeout';
logSh('⏰ Timeout de connexion', 'WARN');
} else {
text.textContent = 'Hors ligne';
logSh(`🚫 Hors ligne: ${error.message}`, 'ERROR');
}
}
}
// Coming soon functionality
function showComingSoon() {
logSh('🚧 Ouverture modal "Bientôt disponible"', 'INFO');
document.getElementById('coming-soon-modal').classList.add('show');
}
function closeModal() {
logSh('❌ Fermeture modal', 'DEBUG');
document.getElementById('coming-soon-modal').classList.remove('show');
}
function showContentCreator() {
logSh('🏭 Ouverture du créateur de contenu', 'INFO');
// Masquer la page d'accueil
document.getElementById('home-page').classList.remove('active');
@ -136,6 +297,7 @@
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
logSh('⌨️ Touche Escape pressée', 'DEBUG');
closeModal();
AppNavigation.goBack();
}

View File

@ -39,22 +39,22 @@ const basicChineseContent = {
{
chinese: "你好!",
english: "Hello!",
pinyin: "Nǐ hǎo!"
prononciation: "Nǐ hǎo!"
},
{
chinese: "我是学生。",
english: "I am a student.",
pinyin: "Wǒ shì xué shēng."
prononciation: "Wǒ shì xué shēng."
},
{
chinese: "谢谢你!",
english: "Thank you!",
pinyin: "Xiè xiè nǐ!"
prononciation: "Xiè xiè nǐ!"
},
{
chinese: "这是我的家。",
english: "This is my home.",
pinyin: "Zhè shì wǒ de jiā."
prononciation: "Zhè shì wǒ de jiā."
}
],
@ -62,10 +62,10 @@ const basicChineseContent = {
{
title: "Basic Greeting",
conversation: [
{ speaker: "A", chinese: "你好!", english: "Hello!", pinyin: "Nǐ hǎo!" },
{ speaker: "B", chinese: "你好!你叫什么名字?", english: "Hello! What's your name?", pinyin: "Nǐ hǎo! Nǐ jiào shén me míng zi?" },
{ speaker: "A", chinese: "我叫小明。你呢?", english: "My name is Xiaoming. And you?", pinyin: "Wǒ jiào Xiǎo Míng. Nǐ ne?" },
{ speaker: "B", chinese: "我叫小红。很高兴认识你!", english: "My name is Xiaohong. Nice to meet you!", pinyin: "Wǒ jiào Xiǎo Hóng. Hěn gāo xìng rèn shi nǐ!" }
{ speaker: "A", chinese: "你好!", english: "Hello!", prononciation: "Nǐ hǎo!" },
{ speaker: "B", chinese: "你好!你叫什么名字?", english: "Hello! What's your name?", prononciation: "Nǐ hǎo! Nǐ jiào shén me míng zi?" },
{ speaker: "A", chinese: "我叫小明。你呢?", english: "My name is Xiaoming. And you?", prononciation: "Wǒ jiào Xiǎo Míng. Nǐ ne?" },
{ speaker: "B", chinese: "我叫小红。很高兴认识你!", english: "My name is Xiaohong. Nice to meet you!", prononciation: "Wǒ jiào Xiǎo Hóng. Hěn gāo xìng rèn shi nǐ!" }
]
}
],
@ -98,7 +98,7 @@ const basicChineseContent = {
window.ContentModules = window.ContentModules || {};
window.ContentModules.BasicChinese = {
name: "Basic Chinese",
description: "Essential Chinese characters, pinyin and vocabulary for beginners",
description: "Essential Chinese characters, pronunciation and vocabulary for beginners",
difficulty: "beginner",
vocabulary: basicChineseContent.vocabulary,
sentences: basicChineseContent.sentences,

View File

@ -1,287 +0,0 @@
// Demo English Class - Rich Content Example
// Shows all possible content types with dummy data
const englishClassDemoContent = {
// Rich vocabulary with all optional features
vocabulary: {
"apple": {
translation: "pomme",
type: "noun",
pronunciation: "audio/apple.mp3",
difficulty: "beginner",
examples: ["I eat an apple every day", "The apple is red and sweet"],
grammarNotes: "Count noun - can be singular or plural"
},
"run": {
translation: "courir",
type: "verb",
pronunciation: "audio/run.mp3",
difficulty: "beginner",
examples: ["I run in the park", "She runs very fast"],
grammarNotes: "Regular verb: run, runs, running, ran"
},
"beautiful": {
translation: "beau/belle",
type: "adjective",
pronunciation: "audio/beautiful.mp3",
difficulty: "intermediate",
examples: ["The sunset is beautiful", "She has beautiful eyes"],
grammarNotes: "Can be used before noun or after 'be'"
},
"hello": {
translation: "bonjour",
type: "greeting",
pronunciation: "audio/hello.mp3",
difficulty: "beginner",
examples: ["Hello, how are you?", "Hello everyone!"],
grammarNotes: "Common greeting used anytime"
},
// Simple format examples (backward compatibility)
"cat": "chat",
"dog": "chien",
"house": "maison"
},
// Grammar rules and explanations
grammar: {
presentSimple: {
title: "Present Simple Tense",
explanation: "Used for habits, facts, and general truths. Form: Subject + base verb (+ s for he/she/it)",
examples: [
{ english: "I walk to school", french: "Je marche à l'école" },
{ english: "She walks to school", french: "Elle marche à l'école" },
{ english: "They walk to school", french: "Ils marchent à l'école" }
],
exercises: [
"Complete: I _____ (play) tennis every Sunday",
"Transform: He walk to work → He _____ to work"
]
},
articles: {
title: "Articles: A, An, The",
explanation: "A/An = indefinite articles (one of many). The = definite article (specific one)",
examples: [
{ english: "I see a cat", french: "Je vois un chat" },
{ english: "I see an elephant", french: "Je vois un éléphant" },
{ english: "I see the cat from yesterday", french: "Je vois le chat d'hier" }
]
}
},
// Audio content with transcripts
audio: {
withText: [
{
title: "Daily Routine Conversation",
audioFile: "audio/daily_routine.mp3",
transcript: "A: What time do you wake up? B: I usually wake up at 7 AM. A: That's early! I wake up at 8:30. B: I like to exercise before work. A: That's a good habit!",
translation: "A: À quelle heure te réveilles-tu? B: Je me réveille habituellement à 7h. A: C'est tôt! Je me réveille à 8h30. B: J'aime faire de l'exercice avant le travail. A: C'est une bonne habitude!",
timestamps: [
{ time: 0.5, text: "What time do you wake up?" },
{ time: 3.2, text: "I usually wake up at 7 AM" },
{ time: 6.8, text: "That's early! I wake up at 8:30" },
{ time: 11.1, text: "I like to exercise before work" },
{ time: 14.5, text: "That's a good habit!" }
]
},
{
title: "Weather Report",
audioFile: "audio/weather.mp3",
transcript: "Today's weather: It's sunny and warm with a high of 25 degrees. Light winds from the south. Perfect day for outdoor activities!",
translation: "Météo d'aujourd'hui: Il fait ensoleillé et chaud avec un maximum de 25 degrés. Vents légers du sud. Journée parfaite pour les activités extérieures!"
}
],
withoutText: [
{
title: "Mystery Conversation",
audioFile: "audio/mystery.mp3",
questions: [
{ question: "How many people are speaking?", type: "ai_interpreted" },
{ question: "What are they talking about?", type: "ai_interpreted" },
{ question: "What is the mood of the conversation?", type: "ai_interpreted" }
]
}
]
},
// Poetry with cultural context
poems: [
{
title: "Roses Are Red",
content: "Roses are red,\nViolets are blue,\nSugar is sweet,\nAnd so are you.",
translation: "Les roses sont rouges,\nLes violettes sont bleues,\nLe sucre est doux,\nEt toi aussi.",
audioFile: "audio/roses_poem.mp3",
culturalContext: "Traditional English nursery rhyme pattern, often used to teach basic rhyming and poetry structure to children."
},
{
title: "Twinkle, Twinkle",
content: "Twinkle, twinkle, little star,\nHow I wonder what you are.\nUp above the world so high,\nLike a diamond in the sky.",
audioFile: "audio/twinkle.mp3",
culturalContext: "Famous children's lullaby, one of the most recognizable songs in English-speaking countries."
}
],
// Fill-in-the-blank exercises
fillInBlanks: [
{
sentence: "I _____ to school every day",
options: ["go", "goes", "going", "went"],
correctAnswer: "go",
explanation: "Present simple with 'I' uses base form of verb"
},
{
sentence: "She _____ a book right now",
options: ["read", "reads", "reading", "is reading"],
correctAnswer: "is reading",
explanation: "Present continuous for actions happening now"
},
{
sentence: "The weather is _____ today",
type: "open_ended",
acceptedAnswers: ["nice", "good", "beautiful", "sunny", "warm", "pleasant", "lovely"],
aiPrompt: "Evaluate if the answer is a positive adjective that could describe good weather"
},
{
sentence: "I feel _____ when I listen to music",
type: "open_ended",
acceptedAnswers: ["happy", "relaxed", "calm", "peaceful", "good", "better"],
aiPrompt: "Check if the answer describes a positive emotion or feeling"
}
],
// Sentence correction exercises
corrections: [
{
incorrect: "I are happy today",
correct: "I am happy today",
explanation: "Use 'am' with pronoun 'I', not 'are'",
type: "grammar_correction"
},
{
incorrect: "She don't like apples",
correct: "She doesn't like apples",
explanation: "Use 'doesn't' with he/she/it, not 'don't'",
type: "grammar_correction"
},
{
incorrect: "I can to swim",
correct: "I can swim",
explanation: "After modal verbs like 'can', use base form without 'to'",
type: "grammar_correction"
}
],
// Reading comprehension with AI evaluation
comprehension: [
{
text: "Sarah is a 25-year-old teacher who lives in London. Every morning, she wakes up at 6:30 AM and goes for a jog in the park near her house. After jogging, she has breakfast and reads the news. She loves her job because she enjoys working with children and helping them learn. On weekends, Sarah likes to visit museums and try new restaurants with her friends.",
questions: [
{
question: "What is Sarah's profession?",
type: "multiple_choice",
options: ["Doctor", "Teacher", "Engineer", "Artist"],
correctAnswer: "Teacher"
},
{
question: "What does Sarah do every morning?",
type: "ai_interpreted",
evaluationPrompt: "Check if answer mentions waking up early, jogging, and having breakfast"
},
{
question: "Why does Sarah love her job?",
type: "ai_interpreted",
evaluationPrompt: "Verify answer mentions working with children and helping them learn"
},
{
question: "How would you describe Sarah's lifestyle?",
type: "ai_interpreted",
evaluationPrompt: "Accept answers mentioning active, healthy, social, or organized lifestyle"
}
]
}
],
// Matching exercises
matching: [
{
title: "Match Animals to Their Sounds",
leftColumn: ["Cat", "Dog", "Cow", "Bird"],
rightColumn: ["Woof", "Meow", "Tweet", "Moo"],
correctPairs: [
{ left: "Cat", right: "Meow" },
{ left: "Dog", right: "Woof" },
{ left: "Cow", right: "Moo" },
{ left: "Bird", right: "Tweet" }
]
},
{
title: "Match Colors in English and French",
leftColumn: ["Red", "Blue", "Green", "Yellow"],
rightColumn: ["Bleu", "Vert", "Rouge", "Jaune"],
correctPairs: [
{ left: "Red", right: "Rouge" },
{ left: "Blue", right: "Bleu" },
{ left: "Green", right: "Vert" },
{ left: "Yellow", right: "Jaune" }
]
}
],
// Standard content (backward compatibility)
sentences: [
{ english: "Hello, how are you?", french: "Bonjour, comment allez-vous?" },
{ english: "I like to read books", french: "J'aime lire des livres" },
{ english: "The weather is nice today", french: "Il fait beau aujourd'hui" },
{ english: "Can you help me please?", french: "Pouvez-vous m'aider s'il vous plaît?" }
],
texts: [
{
title: "My Daily Routine",
content: "I wake up at 7 AM every day. First, I brush my teeth and take a shower. Then I have breakfast with my family. After breakfast, I go to work by bus. I work from 9 AM to 5 PM. In the evening, I cook dinner and watch TV. I go to bed at 10 PM.",
translation: "Je me réveille à 7h tous les jours. D'abord, je me brosse les dents et prends une douche. Ensuite je prends le petit déjeuner avec ma famille. Après le petit déjeuner, je vais au travail en bus. Je travaille de 9h à 17h. Le soir, je cuisine le dîner et regarde la télé. Je me couche à 22h."
},
{
title: "The Four Seasons",
content: "There are four seasons in a year: spring, summer, autumn, and winter. Spring is warm and flowers bloom. Summer is hot and sunny. Autumn is cool and leaves change colors. Winter is cold and it sometimes snows.",
translation: "Il y a quatre saisons dans une année: le printemps, l'été, l'automne et l'hiver. Le printemps est chaud et les fleurs fleurissent. L'été est chaud et ensoleillé. L'automne est frais et les feuilles changent de couleur. L'hiver est froid et il neige parfois."
}
],
dialogues: [
{
title: "At the Restaurant",
conversation: [
{ speaker: "Waiter", english: "Good evening! Welcome to our restaurant.", french: "Bonsoir! Bienvenue dans notre restaurant." },
{ speaker: "Customer", english: "Thank you. Can I see the menu please?", french: "Merci. Puis-je voir le menu s'il vous plaît?" },
{ speaker: "Waiter", english: "Of course! Here you are. What would you like to drink?", french: "Bien sûr! Voici. Que voulez-vous boire?" },
{ speaker: "Customer", english: "I'll have a glass of water, please.", french: "Je prendrai un verre d'eau, s'il vous plaît." }
]
}
]
};
// Export for web module system
window.ContentModules = window.ContentModules || {};
window.ContentModules.EnglishClassDemo = {
name: "English Class Demo",
description: "Complete example with all content types - vocabulary, grammar, audio, poems, exercises",
difficulty: "mixed",
language: "english",
vocabulary: englishClassDemoContent.vocabulary,
grammar: englishClassDemoContent.grammar,
audio: englishClassDemoContent.audio,
poems: englishClassDemoContent.poems,
fillInBlanks: englishClassDemoContent.fillInBlanks,
corrections: englishClassDemoContent.corrections,
comprehension: englishClassDemoContent.comprehension,
matching: englishClassDemoContent.matching,
sentences: englishClassDemoContent.sentences,
texts: englishClassDemoContent.texts,
dialogues: englishClassDemoContent.dialogues
};
// Node.js export (optional)
if (typeof module !== 'undefined' && module.exports) {
module.exports = englishClassDemoContent;
}

View File

@ -0,0 +1,244 @@
{
"name": "English Class Demo",
"description": "Complete example with all content types - vocabulary, grammar, audio, poems, exercises",
"difficulty": "mixed",
"language": "english",
"icon": "🇬🇧",
"vocabulary": {
"apple": {
"translation": "pomme",
"type": "noun",
"pronunciation": "audio/apple.mp3",
"difficulty": "beginner",
"examples": ["I eat an apple every day", "The apple is red and sweet"],
"grammarNotes": "Count noun - can be singular or plural"
},
"run": {
"translation": "courir",
"type": "verb",
"pronunciation": "audio/run.mp3",
"difficulty": "beginner",
"examples": ["I run in the park", "She runs very fast"],
"grammarNotes": "Regular verb: run, runs, running, ran"
},
"beautiful": {
"translation": "beau/belle",
"type": "adjective",
"pronunciation": "audio/beautiful.mp3",
"difficulty": "intermediate",
"examples": ["The sunset is beautiful", "She has beautiful eyes"],
"grammarNotes": "Can be used before noun or after 'be'"
},
"hello": {
"translation": "bonjour",
"type": "greeting",
"pronunciation": "audio/hello.mp3",
"difficulty": "beginner",
"examples": ["Hello, how are you?", "Hello everyone!"],
"grammarNotes": "Common greeting used anytime"
},
"cat": "chat",
"dog": "chien",
"house": "maison"
},
"grammar": {
"presentSimple": {
"title": "Present Simple Tense",
"explanation": "Used for habits, facts, and general truths. Form: Subject + base verb (+ s for he/she/it)",
"examples": [
{ "english": "I walk to school", "french": "Je marche à l'école" },
{ "english": "She walks to school", "french": "Elle marche à l'école" },
{ "english": "They walk to school", "french": "Ils marchent à l'école" }
],
"exercises": [
"Complete: I _____ (play) tennis every Sunday",
"Transform: He walk to work → He _____ to work"
]
},
"articles": {
"title": "Articles: A, An, The",
"explanation": "A/An = indefinite articles (one of many). The = definite article (specific one)",
"examples": [
{ "english": "I see a cat", "french": "Je vois un chat" },
{ "english": "I see an elephant", "french": "Je vois un éléphant" },
{ "english": "I see the cat from yesterday", "french": "Je vois le chat d'hier" }
]
}
},
"audio": {
"withText": [
{
"title": "Daily Routine Conversation",
"audioFile": "audio/daily_routine.mp3",
"transcript": "A: What time do you wake up? B: I usually wake up at 7 AM. A: That's early! I wake up at 8:30. B: I like to exercise before work. A: That's a good habit!",
"translation": "A: À quelle heure te réveilles-tu? B: Je me réveille habituellement à 7h. A: C'est tôt! Je me réveille à 8h30. B: J'aime faire de l'exercice avant le travail. A: C'est une bonne habitude!",
"timestamps": [
{ "time": 0.5, "text": "What time do you wake up?" },
{ "time": 3.2, "text": "I usually wake up at 7 AM" },
{ "time": 6.8, "text": "That's early! I wake up at 8:30" },
{ "time": 11.1, "text": "I like to exercise before work" },
{ "time": 14.5, "text": "That's a good habit!" }
]
},
{
"title": "Weather Report",
"audioFile": "audio/weather.mp3",
"transcript": "Today's weather: It's sunny and warm with a high of 25 degrees. Light winds from the south. Perfect day for outdoor activities!",
"translation": "Météo d'aujourd'hui: Il fait ensoleillé et chaud avec un maximum de 25 degrés. Vents légers du sud. Journée parfaite pour les activités extérieures!"
}
],
"withoutText": [
{
"title": "Mystery Conversation",
"audioFile": "audio/mystery.mp3",
"questions": [
{ "question": "How many people are speaking?", "type": "ai_interpreted" },
{ "question": "What are they talking about?", "type": "ai_interpreted" },
{ "question": "What is the mood of the conversation?", "type": "ai_interpreted" }
]
}
]
},
"poems": [
{
"title": "Roses Are Red",
"content": "Roses are red,\nViolets are blue,\nSugar is sweet,\nAnd so are you.",
"translation": "Les roses sont rouges,\nLes violettes sont bleues,\nLe sucre est doux,\nEt toi aussi.",
"audioFile": "audio/roses_poem.mp3",
"culturalContext": "Traditional English nursery rhyme pattern, often used to teach basic rhyming and poetry structure to children."
},
{
"title": "Twinkle, Twinkle",
"content": "Twinkle, twinkle, little star,\nHow I wonder what you are.\nUp above the world so high,\nLike a diamond in the sky.",
"audioFile": "audio/twinkle.mp3",
"culturalContext": "Famous children's lullaby, one of the most recognizable songs in English-speaking countries."
}
],
"fillInBlanks": [
{
"sentence": "I _____ to school every day",
"options": ["go", "goes", "going", "went"],
"correctAnswer": "go",
"explanation": "Present simple with 'I' uses base form of verb"
},
{
"sentence": "She _____ a book right now",
"options": ["read", "reads", "reading", "is reading"],
"correctAnswer": "is reading",
"explanation": "Present continuous for actions happening now"
},
{
"sentence": "The weather is _____ today",
"type": "open_ended",
"acceptedAnswers": ["nice", "good", "beautiful", "sunny", "warm", "pleasant", "lovely"],
"aiPrompt": "Evaluate if the answer is a positive adjective that could describe good weather"
},
{
"sentence": "I feel _____ when I listen to music",
"type": "open_ended",
"acceptedAnswers": ["happy", "relaxed", "calm", "peaceful", "good", "better"],
"aiPrompt": "Check if the answer describes a positive emotion or feeling"
}
],
"corrections": [
{
"incorrect": "I are happy today",
"correct": "I am happy today",
"explanation": "Use 'am' with pronoun 'I', not 'are'",
"type": "grammar_correction"
},
{
"incorrect": "She don't like apples",
"correct": "She doesn't like apples",
"explanation": "Use 'doesn't' with he/she/it, not 'don't'",
"type": "grammar_correction"
},
{
"incorrect": "I can to swim",
"correct": "I can swim",
"explanation": "After modal verbs like 'can', use base form without 'to'",
"type": "grammar_correction"
}
],
"comprehension": [
{
"text": "Sarah is a 25-year-old teacher who lives in London. Every morning, she wakes up at 6:30 AM and goes for a jog in the park near her house. After jogging, she has breakfast and reads the news. She loves her job because she enjoys working with children and helping them learn. On weekends, Sarah likes to visit museums and try new restaurants with her friends.",
"questions": [
{
"question": "What is Sarah's profession?",
"type": "multiple_choice",
"options": ["Doctor", "Teacher", "Engineer", "Artist"],
"correctAnswer": "Teacher"
},
{
"question": "What does Sarah do every morning?",
"type": "ai_interpreted",
"evaluationPrompt": "Check if answer mentions waking up early, jogging, and having breakfast"
},
{
"question": "Why does Sarah love her job?",
"type": "ai_interpreted",
"evaluationPrompt": "Verify answer mentions working with children and helping them learn"
},
{
"question": "How would you describe Sarah's lifestyle?",
"type": "ai_interpreted",
"evaluationPrompt": "Accept answers mentioning active, healthy, social, or organized lifestyle"
}
]
}
],
"matching": [
{
"title": "Match Animals to Their Sounds",
"leftColumn": ["Cat", "Dog", "Cow", "Bird"],
"rightColumn": ["Woof", "Meow", "Tweet", "Moo"],
"correctPairs": [
{ "left": "Cat", "right": "Meow" },
{ "left": "Dog", "right": "Woof" },
{ "left": "Cow", "right": "Moo" },
{ "left": "Bird", "right": "Tweet" }
]
},
{
"title": "Match Colors in English and French",
"leftColumn": ["Red", "Blue", "Green", "Yellow"],
"rightColumn": ["Bleu", "Vert", "Rouge", "Jaune"],
"correctPairs": [
{ "left": "Red", "right": "Rouge" },
{ "left": "Blue", "right": "Bleu" },
{ "left": "Green", "right": "Vert" },
{ "left": "Yellow", "right": "Jaune" }
]
}
],
"sentences": [
{ "english": "Hello, how are you?", "french": "Bonjour, comment allez-vous?" },
{ "english": "I like to read books", "french": "J'aime lire des livres" },
{ "english": "The weather is nice today", "french": "Il fait beau aujourd'hui" },
{ "english": "Can you help me please?", "french": "Pouvez-vous m'aider s'il vous plaît?" }
],
"texts": [
{
"title": "My Daily Routine",
"content": "I wake up at 7 AM every day. First, I brush my teeth and take a shower. Then I have breakfast with my family. After breakfast, I go to work by bus. I work from 9 AM to 5 PM. In the evening, I cook dinner and watch TV. I go to bed at 10 PM.",
"translation": "Je me réveille à 7h tous les jours. D'abord, je me brosse les dents et prends une douche. Ensuite je prends le petit déjeuner avec ma famille. Après le petit déjeuner, je vais au travail en bus. Je travaille de 9h à 17h. Le soir, je cuisine le dîner et regarde la télé. Je me couche à 22h."
},
{
"title": "The Four Seasons",
"content": "There are four seasons in a year: spring, summer, autumn, and winter. Spring is warm and flowers bloom. Summer is hot and sunny. Autumn is cool and leaves change colors. Winter is cold and it sometimes snows.",
"translation": "Il y a quatre saisons dans une année: le printemps, l'été, l'automne et l'hiver. Le printemps est chaud et les fleurs fleurissent. L'été est chaud et ensoleillé. L'automne est frais et les feuilles changent de couleur. L'hiver est froid et il neige parfois."
}
],
"dialogues": [
{
"title": "At the Restaurant",
"conversation": [
{ "speaker": "Waiter", "english": "Good evening! Welcome to our restaurant.", "french": "Bonsoir! Bienvenue dans notre restaurant." },
{ "speaker": "Customer", "english": "Thank you. Can I see the menu please?", "french": "Merci. Puis-je voir le menu s'il vous plaît?" },
{ "speaker": "Waiter", "english": "Of course! Here you are. What would you like to drink?", "french": "Bien sûr! Voici. Que voulez-vous boire?" },
{ "speaker": "Customer", "english": "I'll have a glass of water, please.", "french": "Je prendrai un verre d'eau, s'il vous plaît." }
]
}
]
}

View File

@ -0,0 +1,213 @@
{
"name": "SBS Level 7-8 (New)",
"description": "Format simple et clair - Homes, Clothing & Cultures",
"difficulty": "intermediate",
"language": "english",
"icon": "🌍",
"vocabulary": {
"central": "中心的;中央的",
"avenue": "大街;林荫道",
"refrigerator": "冰箱",
"closet": "衣柜;壁橱",
"elevator": "电梯",
"building": "建筑物;大楼",
"air conditioner": "空调",
"superintendent": "主管;负责人",
"bus stop": "公交车站",
"jacuzzi": "按摩浴缸",
"machine": "机器;设备",
"two and a half": "两个半",
"in the center of": "在……中心",
"town": "城镇",
"a lot of": "许多",
"noise": "噪音",
"sidewalks": "人行道",
"all day and all night": "整日整夜",
"convenient": "便利的",
"upset": "失望的",
"shirt": "衬衫",
"coat": "外套、大衣",
"dress": "连衣裙",
"skirt": "短裙",
"blouse": "女式衬衫",
"jacket": "夹克、短外套",
"sweater": "毛衣、针织衫",
"suit": "套装、西装",
"tie": "领带",
"pants": "裤子",
"jeans": "牛仔裤",
"belt": "腰带、皮带",
"hat": "帽子",
"glove": "手套",
"purse/pocketbook": "手提包、女式小包",
"glasses": "眼镜",
"pajamas": "睡衣",
"socks": "袜子",
"shoes": "鞋子",
"bathrobe": "浴袍",
"tee shirt": "T恤",
"scarf": "围巾",
"wallet": "钱包",
"ring": "戒指",
"sandals": "凉鞋",
"slippers": "拖鞋",
"sneakers": "运动鞋",
"shorts": "短裤",
"sweat pants": "运动裤",
"urban areas": "cities",
"suburban areas": "places near cities",
"rural areas": "places in the countryside, far from cities",
"farmhouse": "农舍",
"hut": "小屋",
"houseboat": "船屋",
"mobile home": "移动房屋",
"trailer": "拖车房",
"jackets": "夹克",
"gloves": "手套",
"blouses": "女式衬衫",
"bracelets": "手镯",
"ties": "领带"
},
"sentences": [
{
"english": "Amy's apartment building is in the center of town.",
"chinese": "艾米的公寓楼在城镇中心。"
},
{
"english": "There's a lot of noise near Amy's apartment building.",
"chinese": "艾米的公寓楼附近有很多噪音。"
},
{
"english": "It's a very busy place, but it's a convenient place to live.",
"chinese": "那是个非常热闹的地方,但也是个居住很方便的地方。"
},
{
"english": "Around the corner from the building, there are two supermarkets.",
"chinese": "从这栋楼拐个弯,就有两家超市。"
},
{
"english": "I'm looking for a shirt.",
"chinese": "我在找一件衬衫。"
},
{
"english": "Shirts are over there.",
"chinese": "衬衫在那边。"
}
],
"texts": [
{
"title": "People's Homes",
"content": "Homes are different all around the world. This family is living in a farmhouse. This family is living in a hut. This family is living in a houseboat. These people are living in a mobile home (a trailer). What different kinds of homes are there in your country?"
},
{
"title": "Urban, Suburban, and Rural",
"content": "urban areas = cities, suburban areas = places near cities, rural areas = places in the countryside, far from cities. About 50% (percent) of the world's population is in urban and suburban areas. About 50% (percent) of the world's population is in rural areas."
},
{
"title": "Global Exchange - RosieM",
"content": "My apartment is in a wonderful neighborhood. There's a big, beautiful park across from my apartment building. Around the corner, there's a bank, a post office, and a laundromat. There are also many restaurants and stores in my neighborhood. It's a noisy place, but it's a very interesting place. There are a lot of people on the sidewalks all day and all night. How about your neighborhood? Tell me about it."
},
{
"title": "Clothing, Colors, and Cultures",
"content": "Blue and pink aren't children's clothing colors all around the world. The meanings of colors are sometimes very different in different cultures. For example, in some cultures, blue is a common clothing color for little boys, and pink is a common clothing color for little girls. In other cultures, other colors are common for boys and girls. There are also different colors for special days in different cultures. For example, white is the traditional color of a wedding dress in some cultures, but other colors are traditional in other cultures. For some people, white is a happy color. For others, it's a sad color. For some people, red is a beautiful and lucky color. For others, it's a very sad color. What are the meanings of different colors in YOUR culture?"
}
],
"grammar": {
"thereBe": {
"topic": "There be 句型的用法",
"singular": {
"form": "there is (there's) + 名词单数/不可数名词",
"explanation": "在某地方有什么人或东西",
"examples": [
"There's a bank.",
"There's some water.",
"There's a book store on Main Street."
],
"forms": {
"positive": "There's a stove in the kitchen.",
"negative": "There isn't a stove in the kitchen.",
"question": "Is there a stove in the kitchen?",
"shortAnswers": "Yes, there is. / No, there isn't."
}
},
"plural": {
"form": "there are (there're) + 复数名词",
"examples": [
"There're two hospitals.",
"There're many rooms in this apartment."
],
"forms": {
"positive": "There're two windows in the kitchen.",
"negative": "There aren't two windows in the kitchen.",
"question": "Are there two windows in the kitchen?",
"shortAnswers": "Yes, there are. / No, there aren't."
}
}
},
"plurals": {
"topic": "可数名词复数",
"pronunciation": {
"rules": [
{
"condition": "在清辅音/-p,-k/后",
"pronunciation": "/-s/",
"example": "socks中-k是清辅音/-k/,所以-s读/-s/"
},
{
"condition": "在浊辅音和元音音标后",
"pronunciation": "/-z/",
"example": "jeans中-n是浊辅音/-n/, 所以-s读/-z/; tie的读音是/tai/,以元音结尾,所以-s读/-z/"
},
{
"condition": "以/-s,-z,-ʃ,-ʒ,-tʃ,-dʒ/发音结尾的名词",
"pronunciation": "/-iz/",
"example": "watches中-ch读/-tʃ/,所以-es读/-iz/"
}
]
},
"formation": {
"regular": {
"rule": "一般在词尾加-s",
"examples": ["shirts", "shoes"]
},
"special": {
"rule": "以-s,-sh,-ch,-x,以及辅音字母o结尾的词在词尾加-es",
"examples": ["boxes", "buses", "potatoes", "tomatoes", "heroes"]
},
"irregular": {
"rule": "特殊的复数形式",
"examples": {
"man": "men",
"woman": "women",
"child": "children",
"tooth": "teeth",
"mouse": "mice"
}
}
}
}
},
"listening": {
"jMartShopping": {
"title": "Attention, J-Mart Shoppers!",
"items": [
{ "item": "jackets", "aisle": "Aisle 9" },
{ "item": "gloves", "aisle": "Aisle 7" },
{ "item": "blouses", "aisle": "Aisle 9" },
{ "item": "bracelets", "aisle": "Aisle 11" },
{ "item": "ties", "aisle": "Aisle 5" }
]
}
},
"exercises": {
"sentenceCompletion": [
"That's a very nice _______.",
"Those are very nice _______."
],
"questions": [
"What different kinds of homes are there in your country?",
"How about your neighborhood? Tell me about it.",
"What are the meanings of different colors in YOUR culture?"
]
}
}

102
js/content/test-animals.js Normal file
View File

@ -0,0 +1,102 @@
// Test Animals Content - Contenu bidon pour tester le scanner automatique
const testAnimalsContent = {
vocabulary: {
"cat": {
translation: "chat",
type: "noun",
difficulty: "beginner",
examples: ["I have a cat", "The cat is sleeping"],
grammarNotes: "Count noun - singular: cat, plural: cats"
},
"dog": {
translation: "chien",
type: "noun",
difficulty: "beginner",
examples: ["My dog is friendly", "Dogs love to play"],
grammarNotes: "Count noun - singular: dog, plural: dogs"
},
"bird": {
translation: "oiseau",
type: "noun",
difficulty: "beginner",
examples: ["The bird can fly", "Birds sing in the morning"],
grammarNotes: "Count noun - singular: bird, plural: birds"
},
"fish": {
translation: "poisson",
type: "noun",
difficulty: "beginner",
examples: ["Fish live in water", "I caught a fish"],
grammarNotes: "Count/mass noun - same form for singular and plural"
},
"elephant": {
translation: "éléphant",
type: "noun",
difficulty: "intermediate",
examples: ["Elephants are very big", "The elephant has a long trunk"],
grammarNotes: "Count noun - uses 'an' article due to vowel sound"
}
},
sentences: [
{
english: "I love animals",
french: "J'aime les animaux",
prononciation: "I love animals"
},
{
english: "Cats and dogs are pets",
french: "Les chats et les chiens sont des animaux de compagnie",
prononciation: "Cats and dogs are pets"
},
{
english: "Birds can fly high in the sky",
french: "Les oiseaux peuvent voler haut dans le ciel",
prononciation: "Birds can fly high in the sky"
}
],
dialogues: [
{
title: "At the Zoo",
conversation: [
{ speaker: "Child", english: "Look! What animal is that?", french: "Regarde ! Quel animal est-ce ?" },
{ speaker: "Parent", english: "That's an elephant", french: "C'est un éléphant" },
{ speaker: "Child", english: "It's so big!", french: "Il est si grand !" },
{ speaker: "Parent", english: "Yes, elephants are the largest land animals", french: "Oui, les éléphants sont les plus grands animaux terrestres" }
]
}
],
grammar: {
animalPlurals: {
title: "Animal Plurals",
explanation: "Most animal names follow regular plural rules: add -s. Some have irregular plurals.",
examples: [
{ english: "One cat, two cats", french: "Un chat, deux chats" },
{ english: "One fish, many fish", french: "Un poisson, plusieurs poissons" },
{ english: "One mouse, two mice", french: "Une souris, deux souris" }
]
}
}
};
// Export for web module system
window.ContentModules = window.ContentModules || {};
window.ContentModules.TestAnimals = {
name: "Test Animals",
description: "Basic animal vocabulary for testing - will be auto-discovered!",
difficulty: "beginner",
language: "english",
vocabulary: testAnimalsContent.vocabulary,
sentences: testAnimalsContent.sentences,
dialogues: testAnimalsContent.dialogues,
grammar: testAnimalsContent.grammar,
icon: "🐾"
};
// Node.js export (optional)
if (typeof module !== 'undefined' && module.exports) {
module.exports = testAnimalsContent;
}

355
js/core/browser-logger.js Normal file
View File

@ -0,0 +1,355 @@
// ========================================
// FICHIER: js/core/browser-logger.js - VERSION NAVIGATEUR
// Système de logging adapté du système ErrorReporting.js pour le navigateur
// ========================================
class BrowserLogger {
constructor() {
this.logs = [];
this.maxLogs = 1000; // Limite pour éviter l'explosion mémoire
this.logContainer = null;
this.isVisible = false;
// Niveaux de log avec couleurs
this.levels = {
TRACE: { value: 10, color: '#6c757d', emoji: '🔍' },
DEBUG: { value: 20, color: '#007bff', emoji: '🐛' },
INFO: { value: 30, color: '#28a745', emoji: '' },
WARN: { value: 40, color: '#ffc107', emoji: '⚠️' },
ERROR: { value: 50, color: '#dc3545', emoji: '❌' }
};
this.minLevel = 'DEBUG'; // Niveau minimum affiché
this.init();
}
init() {
// Créer l'interface de log
this.createLogUI();
// Intercepter les erreurs globales
window.addEventListener('error', (event) => {
this.logSh(`Erreur JavaScript: ${event.error.message}`, 'ERROR');
});
window.addEventListener('unhandledrejection', (event) => {
this.logSh(`Promise rejetée: ${event.reason}`, 'ERROR');
});
logSh('🚀 BrowserLogger initialisé', 'INFO');
}
createLogUI() {
// Créer le conteneur de log
const logContainer = document.createElement('div');
logContainer.id = 'browser-logger';
logContainer.innerHTML = `
<div class="logger-header">
<h3>📋 System Logs</h3>
<div class="logger-controls">
<select id="log-level-filter">
<option value="TRACE">TRACE</option>
<option value="DEBUG" selected>DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARN">WARN</option>
<option value="ERROR">ERROR</option>
</select>
<button id="clear-logs">🗑</button>
<button id="toggle-logger"></button>
</div>
</div>
<div class="logger-content" id="logger-content">
<div class="log-entry info">
<span class="log-time">${this.formatTime(new Date())}</span>
<span class="log-level">INFO</span>
<span class="log-message">Logger initialisé</span>
</div>
</div>
`;
// Ajouter les styles CSS
const style = document.createElement('style');
style.textContent = `
#browser-logger {
position: fixed;
top: 80px;
right: 10px;
width: 400px;
max-height: 500px;
background: #fff;
border: 2px solid #007bff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
z-index: 10000;
display: none;
}
#browser-logger.visible {
display: block;
}
.logger-header {
background: #007bff;
color: white;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 6px 6px 0 0;
}
.logger-header h3 {
margin: 0;
font-size: 14px;
}
.logger-controls {
display: flex;
gap: 5px;
align-items: center;
}
.logger-controls select {
padding: 2px 4px;
font-size: 11px;
border: none;
border-radius: 3px;
}
.logger-controls button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.logger-controls button:hover {
background: rgba(255,255,255,0.3);
}
.logger-content {
max-height: 400px;
overflow-y: auto;
padding: 0;
background: #f8f9fa;
}
.log-entry {
padding: 4px 8px;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 8px;
font-size: 11px;
line-height: 1.4;
}
.log-entry:hover {
background: #e9ecef;
}
.log-time {
color: #6c757d;
min-width: 60px;
font-weight: bold;
}
.log-level {
min-width: 50px;
font-weight: bold;
text-align: center;
border-radius: 3px;
padding: 1px 4px;
}
.log-level.trace { background: #f8f9fa; color: #6c757d; }
.log-level.debug { background: #cce5ff; color: #007bff; }
.log-level.info { background: #d4edda; color: #28a745; }
.log-level.warn { background: #fff3cd; color: #856404; }
.log-level.error { background: #f8d7da; color: #721c24; }
.log-message {
flex: 1;
word-break: break-word;
}
.log-message.trace { color: #6c757d; }
.log-message.network { color: #17a2b8; font-weight: bold; }
.log-message.auth { color: #6610f2; font-weight: bold; }
`;
document.head.appendChild(style);
document.body.appendChild(logContainer);
this.logContainer = logContainer;
this.logContent = logContainer.querySelector('#logger-content');
// Event listeners
document.getElementById('toggle-logger').addEventListener('click', () => {
this.toggle();
});
document.getElementById('clear-logs').addEventListener('click', () => {
this.clear();
});
document.getElementById('log-level-filter').addEventListener('change', (e) => {
this.minLevel = e.target.value;
this.refreshDisplay();
});
// Raccourci clavier pour toggle
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'L') {
this.toggle();
}
});
}
formatTime(date) {
return date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
logSh(message, level = 'INFO') {
const timestamp = new Date();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
id: Date.now() + Math.random()
};
// Ajouter à la liste des logs
this.logs.push(logEntry);
// Limiter le nombre de logs en mémoire
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Log aussi dans la console navigateur (gardé pour le debugging)
if (level !== 'TRACE' && level !== 'DEBUG') { // Éviter le spam en console
const consoleMethod = level.toLowerCase() === 'error' ? 'error' :
level.toLowerCase() === 'warn' ? 'warn' : 'log';
console[consoleMethod](`[${this.formatTime(timestamp)}] ${level}: ${message}`);
}
// Mettre à jour l'affichage si visible
if (this.isVisible) {
this.addLogToDisplay(logEntry);
}
return logEntry;
}
addLogToDisplay(logEntry) {
const levelInfo = this.levels[logEntry.level] || this.levels.INFO;
// Vérifier si on doit afficher ce niveau
if (levelInfo.value < this.levels[this.minLevel].value) {
return;
}
const logElement = document.createElement('div');
logElement.className = `log-entry ${logEntry.level.toLowerCase()}`;
logElement.innerHTML = `
<span class="log-time">${this.formatTime(logEntry.timestamp)}</span>
<span class="log-level ${logEntry.level.toLowerCase()}">${logEntry.level}</span>
<span class="log-message ${this.getMessageClass(logEntry.message)}">${this.formatMessage(logEntry.message)}</span>
`;
this.logContent.appendChild(logElement);
// Auto-scroll vers le bas
this.logContent.scrollTop = this.logContent.scrollHeight;
// Limiter les éléments DOM affichés
const entries = this.logContent.querySelectorAll('.log-entry');
if (entries.length > 200) {
entries[0].remove();
}
}
getMessageClass(message) {
if (message.includes('🌐') || message.includes('📡') || message.includes('connexion')) {
return 'network';
}
if (message.includes('🔐') || message.includes('auth') || message.includes('Headers')) {
return 'auth';
}
return '';
}
formatMessage(message) {
// Échapper le HTML mais garder les emojis
return message.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
refreshDisplay() {
if (!this.isVisible) return;
this.logContent.innerHTML = '';
this.logs.forEach(logEntry => {
this.addLogToDisplay(logEntry);
});
}
show() {
this.isVisible = true;
this.logContainer.classList.add('visible');
this.refreshDisplay();
this.logSh('Logger affiché', 'DEBUG');
}
hide() {
this.isVisible = false;
this.logContainer.classList.remove('visible');
this.logSh('Logger masqué', 'DEBUG');
}
toggle() {
if (this.isVisible) {
this.hide();
} else {
this.show();
}
}
clear() {
this.logs = [];
if (this.logContent) {
this.logContent.innerHTML = '';
}
this.logSh('Logs effacés', 'INFO');
}
// Méthodes de convenance
trace(message) { return this.logSh(message, 'TRACE'); }
debug(message) { return this.logSh(message, 'DEBUG'); }
info(message) { return this.logSh(message, 'INFO'); }
warn(message) { return this.logSh(message, 'WARN'); }
error(message) { return this.logSh(message, 'ERROR'); }
}
// Instance globale
window.logger = new BrowserLogger();
// Fonction globale compatible avec le système original
window.logSh = (message, level = 'INFO') => {
return window.logger.logSh(message, level);
};
// Export pour compatibilité
if (typeof module !== 'undefined' && module.exports) {
module.exports = { BrowserLogger };
}

View File

@ -25,7 +25,7 @@ class ContentEngine {
return processedContent;
} catch (error) {
console.error(`Erreur chargement contenu ${contentId}:`, error);
logSh(`Erreur chargement contenu ${contentId}:`, error, 'ERROR');
throw error;
}
}
@ -45,7 +45,7 @@ class ContentEngine {
processContent(rawContent) {
// Vérifier le format
if (this.isOldFormat(rawContent)) {
console.log('Migration ancien format vers nouveau format...');
logSh('Migration ancien format vers nouveau format...', 'INFO');
return this.migrator.migrateToNewFormat(rawContent);
}
@ -196,7 +196,7 @@ class ContentMigrator {
// Configuration
config: {
defaultInteraction: 'click',
supportedGames: ['whack-a-mole', 'memory-game', 'temp-games'],
supportedGames: ['whack-a-mole', 'memory-game'],
adaptiveEnabled: true
},
@ -343,12 +343,12 @@ class ContentValidator {
try {
// Vérifications de base
if (!content.id || !content.name) {
console.warn('Contenu manque ID ou nom');
logSh('Contenu manque ID ou nom', 'WARN');
return false;
}
if (!content.contentItems || !Array.isArray(content.contentItems)) {
console.warn('contentItems manquant ou invalide');
logSh('contentItems manquant ou invalide', 'WARN');
return false;
}
@ -361,7 +361,7 @@ class ContentValidator {
return true;
} catch (error) {
console.error('Erreur validation:', error);
logSh('Erreur validation:', error, 'ERROR');
return false;
}
}
@ -372,7 +372,7 @@ class ContentValidator {
for (let field of requiredFields) {
if (!item[field]) {
console.warn(`Champ requis manquant: ${field}`);
logSh(`Champ requis manquant: ${field}`, 'WARN');
return false;
}
}
@ -386,7 +386,7 @@ class ContentValidator {
case 'dialogue':
return this.validateDialogue(item);
default:
console.warn(`Type de contenu inconnu: ${item.type}`);
logSh(`Type de contenu inconnu: ${item.type}`, 'WARN');
return true; // Permettre types inconnus pour extensibilité
}
}
@ -417,8 +417,6 @@ class GameContentAdapter {
return this.adaptForWhackAMole(content);
case 'memory-game':
return this.adaptForMemoryGame(content);
case 'temp-games':
return this.adaptForTempGames(content);
default:
return content;
}

View File

@ -66,7 +66,7 @@ class ContentFactory {
requiredFields: ['english', 'french'],
optionalFields: ['image', 'audio', 'phonetic', 'category'],
interactions: ['click', 'drag_drop', 'type'],
games: ['whack-a-mole', 'memory-game', 'temp-games']
games: ['whack-a-mole', 'memory-game']
});
this.templates.set('dialogue_conversation', {
@ -75,7 +75,7 @@ class ContentFactory {
requiredFields: ['speakers', 'conversation'],
optionalFields: ['scenario', 'context', 'audio_files'],
interactions: ['role_play', 'click', 'build_sentence'],
games: ['story-builder', 'temp-games']
games: ['story-builder']
});
this.templates.set('sequence_story', {
@ -93,7 +93,7 @@ class ContentFactory {
requiredFields: ['setting', 'vocabulary', 'phrases'],
optionalFields: ['roles', 'objectives', 'media'],
interactions: ['simulation', 'role_play', 'click'],
games: ['story-builder', 'temp-games']
games: ['story-builder']
});
}
@ -101,7 +101,7 @@ class ContentFactory {
async createContent(input, options = {}) {
try {
console.log('🏭 Content Factory - Début création contenu');
logSh('🏭 Content Factory - Début création contenu', 'INFO');
// 1. Analyser l'input
const parsedContent = await this.parseInput(input, options);
@ -123,11 +123,11 @@ class ContentFactory {
throw new Error('Contenu généré invalide');
}
console.log('✅ Content Factory - Contenu créé avec succès');
logSh('✅ Content Factory - Contenu créé avec succès', 'INFO');
return contentModule;
} catch (error) {
console.error('❌ Content Factory - Erreur:', error);
logSh('❌ Content Factory - Erreur:', error, 'ERROR');
throw error;
}
}
@ -246,7 +246,7 @@ class ContentFactory {
config: {
defaultInteraction: options.defaultInteraction || "click",
supportedGames: options.supportedGames || ["whack-a-mole", "memory-game", "temp-games", "story-builder"],
supportedGames: options.supportedGames || ["whack-a-mole", "memory-game", "story-builder"],
adaptiveEnabled: true,
difficultyProgression: true
},
@ -510,7 +510,7 @@ class MediaProcessor {
processedMedia.images[file.id || file.name] = processed;
}
} catch (error) {
console.warn(`Erreur traitement fichier ${file.name}:`, error);
logSh(`Erreur traitement fichier ${file.name}:`, error, 'WARN');
}
}

View File

@ -3,7 +3,7 @@
// === GÉNÉRATEUR DE VOCABULAIRE ===
class VocabularyGenerator {
async generate(parsedContent, options = {}) {
console.log('📚 VocabularyGenerator - Génération exercices vocabulaire');
logSh('📚 VocabularyGenerator - Génération exercices vocabulaire', 'INFO');
const exercises = [];
const vocabulary = parsedContent.vocabulary || [];
@ -198,7 +198,7 @@ class VocabularyGenerator {
// === GÉNÉRATEUR DE PHRASES ===
class SentenceGenerator {
async generate(parsedContent, options = {}) {
console.log('📖 SentenceGenerator - Génération exercices phrases');
logSh('📖 SentenceGenerator - Génération exercices phrases', 'INFO');
const exercises = [];
const sentences = parsedContent.sentences || [];
@ -357,7 +357,7 @@ class SentenceGenerator {
// === GÉNÉRATEUR DE DIALOGUES ===
class DialogueGenerator {
async generate(parsedContent, options = {}) {
console.log('💬 DialogueGenerator - Génération exercices dialogues');
logSh('💬 DialogueGenerator - Génération exercices dialogues', 'INFO');
const exercises = [];
@ -582,7 +582,7 @@ class DialogueGenerator {
// === GÉNÉRATEUR DE SÉQUENCES ===
class SequenceGenerator {
async generate(parsedContent, options = {}) {
console.log('📋 SequenceGenerator - Génération exercices séquences');
logSh('📋 SequenceGenerator - Génération exercices séquences', 'INFO');
const exercises = [];
@ -793,7 +793,7 @@ class SequenceGenerator {
// === GÉNÉRATEUR AUTOMATIQUE ===
class AutoGenerator {
async generate(parsedContent, options = {}) {
console.log('🤖 AutoGenerator - Génération automatique intelligente');
logSh('🤖 AutoGenerator - Génération automatique intelligente', 'INFO');
const exercises = [];

View File

@ -3,7 +3,7 @@
// === PARSER DE TEXTE LIBRE ===
class TextParser {
async parse(text, options = {}) {
console.log('📝 TextParser - Analyse du texte libre');
logSh('📝 TextParser - Analyse du texte libre', 'INFO');
const result = {
rawText: text,
@ -165,7 +165,7 @@ class TextParser {
// === PARSER CSV ===
class CSVParser {
async parse(csvText, options = {}) {
console.log('📊 CSVParser - Analyse CSV');
logSh('📊 CSVParser - Analyse CSV', 'INFO');
const separator = options.separator || this.detectSeparator(csvText);
const lines = csvText.split('\n').filter(line => line.trim());
@ -214,7 +214,7 @@ class CSVParser {
// === PARSER JSON ===
class JSONParser {
async parse(jsonData, options = {}) {
console.log('🔗 JSONParser - Analyse JSON');
logSh('🔗 JSONParser - Analyse JSON', 'INFO');
let data;
if (typeof jsonData === 'string') {
@ -238,7 +238,7 @@ class JSONParser {
// === PARSER DIALOGUE SPÉCIALISÉ ===
class DialogueParser {
async parse(dialogueText, options = {}) {
console.log('💬 DialogueParser - Analyse dialogue');
logSh('💬 DialogueParser - Analyse dialogue', 'INFO');
const scenes = this.extractScenes(dialogueText);
const characters = this.extractCharacters(dialogueText);
@ -310,7 +310,7 @@ class DialogueParser {
// === PARSER SÉQUENCE SPÉCIALISÉ ===
class SequenceParser {
async parse(sequenceText, options = {}) {
console.log('📋 SequenceParser - Analyse séquence');
logSh('📋 SequenceParser - Analyse séquence', 'INFO');
const title = this.extractTitle(sequenceText);
const steps = this.extractSteps(sequenceText);
@ -403,7 +403,7 @@ class SequenceParser {
// === PARSER MÉDIA ===
class MediaParser {
async parse(mediaData, options = {}) {
console.log('🎵 MediaParser - Analyse médias');
logSh('🎵 MediaParser - Analyse médias', 'INFO');
const result = {
audio: [],

View File

@ -3,16 +3,15 @@
class ContentScanner {
constructor() {
this.discoveredContent = new Map();
this.contentFiles = [
// Liste des fichiers de contenu à scanner automatiquement
'sbs-level-7-8-new.js',
'basic-chinese.js',
'english-class-demo.js'
];
this.contentDirectory = 'js/content/';
// Configuration depuis EnvConfig
this.envConfig = window.envConfig;
logSh('🔧 ContentScanner configuré avec:', 'INFO');
}
async scanAllContent() {
console.log('🔍 ContentScanner - Scan automatique du contenu...');
logSh('🔍 ContentScanner - Scan automatique du dossier content...', 'INFO');
const results = {
found: [],
@ -20,25 +19,325 @@ class ContentScanner {
total: 0
};
for (const filename of this.contentFiles) {
try {
const contentInfo = await this.scanContentFile(filename);
if (contentInfo) {
this.discoveredContent.set(contentInfo.id, contentInfo);
results.found.push(contentInfo);
try {
// Découvrir tous les fichiers .js dans le dossier content
const contentFiles = await this.discoverContentFiles();
logSh(`📁 Fichiers trouvés: ${contentFiles.join(', ')}`, 'INFO');
for (const filename of contentFiles) {
try {
const contentInfo = await this.scanContentFile(filename);
if (contentInfo) {
this.discoveredContent.set(contentInfo.id, contentInfo);
results.found.push(contentInfo);
}
} catch (error) {
logSh(`⚠️ Erreur scan ${filename}:`, error.message, 'WARN');
results.errors.push({ filename, error: error.message });
}
} catch (error) {
console.warn(`⚠️ Erreur scan ${filename}:`, error.message);
results.errors.push({ filename, error: error.message });
}
} catch (error) {
logSh('❌ Erreur lors de la découverte des fichiers:', error.message, 'ERROR');
results.errors.push({ error: `Scan directory failed: ${error.message}` });
}
results.total = results.found.length;
console.log(`✅ Scan terminé: ${results.total} modules trouvés`);
logSh(`✅ Scan terminé: ${results.total} modules trouvés`, 'INFO');
return results;
}
async discoverContentFiles() {
// Détecter si on est en mode file:// ou serveur web
const isFileProtocol = window.location.protocol === 'file:';
if (isFileProtocol) {
logSh('📂 Mode fichier local - chargement des fichiers connus', 'INFO');
// D'abord essayer de charger les fichiers connus
await this.preloadKnownFiles();
// Puis scanner les modules chargés
return this.scanLoadedModules();
}
// Méthode 1: Essayer de récupérer le listing via fetch (si serveur web supporte)
try {
const response = await fetch(this.contentDirectory);
if (response.ok) {
const html = await response.text();
// Parser les liens .js dans le HTML du directory listing
const jsFiles = this.parseDirectoryListing(html);
if (jsFiles.length > 0) {
logSh('📂 Méthode directory listing réussie', 'INFO');
return jsFiles;
}
}
} catch (error) {
logSh('📂 Directory listing failed, trying known files', 'INFO');
}
// Méthode 2: Essayer une liste de fichiers communs
logSh('📂 Utilisation de la liste de test', 'INFO');
return await this.tryCommonFiles();
}
async preloadKnownFiles() {
const knownFiles = [
'sbs-level-7-8-new.json', // Format JSON
'basic-chinese.js',
'english-class-demo.json', // Format JSON
'test-animals.js'
];
logSh('📂 Préchargement des fichiers connus...', 'INFO');
this.updateConnectionStatus('loading');
let remoteSuccess = 0;
let localSuccess = 0;
for (const filename of knownFiles) {
try {
if (filename.endsWith('.json')) {
// Essayer d'abord le contenu distant, puis local en fallback
const success = await this.loadJsonWithFallback(filename);
if (success === 'remote') remoteSuccess++;
else if (success === 'local') localSuccess++;
} else {
// Fichiers JS en local uniquement
await this.loadScript(`${this.contentDirectory}${filename}`);
localSuccess++;
}
logSh(`✓ Chargé: ${filename}`, 'INFO');
} catch (error) {
logSh(`⚠️ Ignoré: ${filename} (${error.message})`, 'INFO');
}
}
// Mise à jour du statut de connexion
if (remoteSuccess > 0) {
this.updateConnectionStatus('online', `${remoteSuccess} contenus distants`);
} else if (localSuccess > 0) {
this.updateConnectionStatus('offline', `${localSuccess} contenus locaux`);
} else {
this.updateConnectionStatus('error', 'Aucun contenu chargé');
}
}
async loadJsonWithFallback(filename) {
const tryRemoteFirst = this.envConfig?.get('TRY_REMOTE_FIRST') && this.shouldTryRemote();
if (tryRemoteFirst) {
// Mode distant prioritaire (rare)
const remoteResult = await this.tryRemoteLoad(filename);
if (remoteResult.success) return remoteResult.source;
// Fallback local
const localResult = await this.tryLocalLoad(filename);
if (localResult.success) return localResult.source;
throw new Error(`Impossible de charger ${filename}: Remote (${remoteResult.error}) et Local (${localResult.error})`);
} else {
// Mode LOCAL PRIORITAIRE (par défaut)
const localResult = await this.tryLocalLoad(filename);
if (localResult.success) return localResult.source;
// Fallback distant seulement si configuré et réseau disponible
if (this.shouldTryRemote()) {
const remoteResult = await this.tryRemoteLoad(filename);
if (remoteResult.success) return remoteResult.source;
throw new Error(`Impossible de charger ${filename}: Local (${localResult.error}) et Remote (${remoteResult.error})`);
} else {
throw new Error(`Impossible de charger ${filename}: ${localResult.error} (distant désactivé)`);
}
}
}
async tryLocalLoad(filename) {
try {
const localUrl = `${this.contentDirectory}${filename}`;
await this.loadJsonContent(localUrl);
logSh(`💾 Chargé depuis local: ${filename}`, 'INFO');
return { success: true, source: 'local' };
} catch (error) {
logSh(`💾 Local échoué pour ${filename}: ${error.message}`, 'INFO');
return { success: false, error: error.message };
}
}
async tryRemoteLoad(filename) {
try {
const remoteUrl = `${this.envConfig.getRemoteContentUrl()}${filename}`;
// Fetch avec timeout court
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.envConfig.get('REMOTE_TIMEOUT'));
await this.loadJsonContentWithTimeout(remoteUrl, controller.signal);
clearTimeout(timeoutId);
logSh(`🌐 Chargé depuis distant: ${filename}`, 'INFO');
return { success: true, source: 'remote' };
} catch (error) {
logSh(`🌐 Distant échoué pour ${filename}: ${error.message}`, 'INFO');
return { success: false, error: error.message };
}
}
async loadJsonContentWithTimeout(url, signal) {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const jsonData = await response.json();
const moduleName = this.jsonFilenameToModuleName(url);
window.ContentModules = window.ContentModules || {};
window.ContentModules[moduleName] = jsonData;
}
shouldTryRemote() {
// Utiliser la configuration de EnvConfig
return this.envConfig?.isRemoteContentEnabled() &&
(window.location.protocol !== 'file:' || this.envConfig.get('FORCE_REMOTE_ON_FILE'));
}
updateConnectionStatus(status, details = '') {
// Émettre un événement personnalisé pour l'UI
const event = new CustomEvent('contentConnectionStatus', {
detail: { status, details, timestamp: new Date() }
});
window.dispatchEvent(event);
}
scanLoadedModules() {
// Scanner les modules déjà présents dans window.ContentModules
const loadedFiles = [];
if (window.ContentModules) {
for (const moduleName in window.ContentModules) {
// Convertir le nom du module en nom de fichier probable
const filename = this.moduleNameToFilename(moduleName);
loadedFiles.push(filename);
logSh(`✓ Module découvert: ${moduleName}${filename}`, 'INFO');
}
}
return loadedFiles;
}
moduleNameToFilename(moduleName) {
// Mapping des noms de modules vers les noms de fichiers
const mapping = {
'SBSLevel78New': 'sbs-level-7-8-new.json',
'BasicChinese': 'basic-chinese.js',
'EnglishClassDemo': 'english-class-demo.json',
'TestAnimals': 'test-animals.js'
};
if (mapping[moduleName]) {
return mapping[moduleName];
}
// Conversion générique PascalCase → kebab-case
return moduleName
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase() + '.js';
}
parseDirectoryListing(html) {
const jsFiles = [];
// Regex pour trouver les liens vers des fichiers .js
const linkRegex = /<a[^>]+href="([^"]+\.js)"[^>]*>/gi;
let match;
while ((match = linkRegex.exec(html)) !== null) {
const filename = match[1];
// Éviter les fichiers système ou temporaires
if (!filename.startsWith('.') && !filename.includes('test') && !filename.includes('backup')) {
jsFiles.push(filename);
}
}
return jsFiles;
}
async tryCommonFiles() {
// Liste des fichiers à tester (sera étendue dynamiquement)
const possibleFiles = [
'sbs-level-7-8-new.js',
'sbs-level-7-8-new.json',
'basic-chinese.js',
'sbs-level-8.js',
'animals.js',
'colors.js',
'family.js',
'food.js',
'house.js',
'english-basic.js',
'french-basic.js',
'spanish-basic.js',
'english-class-demo.json',
'test-animals.js'
];
const existingFiles = [];
for (const filename of possibleFiles) {
try {
// Tester local d'abord
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1000);
const response = await fetch(`${this.contentDirectory}${filename}`, {
method: 'HEAD',
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
existingFiles.push(filename);
logSh(`✓ Trouvé (local): ${filename}`, 'INFO');
continue;
}
} catch (error) {
// Fichier local n'existe pas
}
// Si pas trouvé en local et remote activé, tester remote
if (this.shouldTryRemoteContent()) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const remoteUrl = `${this.envConfig.getRemoteContentUrl()}${filename}`;
const authHeaders = await this.envConfig.getAuthHeaders('HEAD', remoteUrl);
const response = await fetch(remoteUrl, {
method: 'HEAD',
signal: controller.signal,
headers: authHeaders
});
clearTimeout(timeoutId);
if (response.ok) {
existingFiles.push(filename);
logSh(`✓ Trouvé (remote): ${filename}`, 'INFO');
} else if (response.status === 403) {
logSh(`🔒 Trouvé mais privé (remote): ${filename}`, 'INFO');
// Même si privé, on considère que le fichier existe
existingFiles.push(filename);
}
} catch (error) {
// Fichier remote n'existe pas ou timeout
}
}
}
return existingFiles;
}
async scanContentFile(filename) {
const contentId = this.extractContentId(filename);
const moduleName = this.getModuleName(contentId);
@ -57,7 +356,7 @@ class ContentScanner {
// Extraire les métadonnées
const contentInfo = this.extractContentInfo(module, contentId, filename);
console.log(`📦 Contenu découvert: ${contentInfo.name}`);
logSh(`📦 Contenu découvert: ${contentInfo.name}`, 'INFO');
return contentInfo;
} catch (error) {
@ -264,7 +563,6 @@ class ContentScanner {
'whack-a-mole': { compatible: false, score: 0 },
'memory-game': { compatible: false, score: 0 },
'story-builder': { compatible: false, score: 0 },
'temp-games': { compatible: false, score: 0 }
};
const vocabCount = this.countByType(module, 'vocabulary');
@ -289,15 +587,37 @@ class ContentScanner {
compatibility['story-builder'].score = Math.min(100, dialogueCount * 15 + sentenceCount * 2);
}
// Temp Games - accepte tout
if (this.countItems(module) > 3) {
compatibility['temp-games'].compatible = true;
compatibility['temp-games'].score = Math.min(100, this.countItems(module) * 2);
}
return compatibility;
}
async loadJsonContent(src) {
try {
const response = await fetch(src);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const jsonData = await response.json();
// Créer le module automatiquement
const moduleName = this.jsonFilenameToModuleName(src);
window.ContentModules = window.ContentModules || {};
window.ContentModules[moduleName] = jsonData;
logSh(`📋 Module JSON créé: ${moduleName}`, 'INFO');
} catch (error) {
throw new Error(`Impossible de charger JSON ${src}: ${error.message}`);
}
}
jsonFilenameToModuleName(src) {
// Extraire le nom du fichier et le convertir en PascalCase
const filename = src.split('/').pop().replace('.json', '');
return this.toPascalCase(filename);
}
async loadScript(src) {
return new Promise((resolve, reject) => {
// Vérifier si déjà chargé

269
js/core/env-config.js Normal file
View File

@ -0,0 +1,269 @@
// === CONFIGURATION ENVIRONNEMENT ===
// Configuration basée sur les variables d'environnement
class EnvConfig {
constructor() {
this.config = {
// DigitalOcean Spaces Configuration
DO_ENDPOINT: 'https://autocollant.fra1.digitaloceanspaces.com',
DO_CONTENT_PATH: 'Class_generator/ContentMe',
// Authentification DigitalOcean Spaces (depuis .env)
DO_ACCESS_KEY: 'DO801XTYPE968NZGAQM3',
DO_SECRET_KEY: '5aCCBiS9K+J8gsAe3M3/0GlliHCNjtLntwla1itCN1s',
DO_REGION: 'fra1',
// Content loading configuration - PRIORITÉ AU LOCAL
USE_REMOTE_CONTENT: true, // Activé maintenant qu'on a les clés
FALLBACK_TO_LOCAL: true, // TOUJOURS essayer local
TRY_REMOTE_FIRST: false, // Essayer local d'abord
REMOTE_TIMEOUT: 3000, // Timeout rapide pour éviter les attentes
// Debug et logging
DEBUG_MODE: true, // Activé pour debugging
LOG_CONTENT_LOADING: true
};
this.remoteContentUrl = this.buildContentUrl();
if (typeof logSh !== 'undefined') {
logSh(`🔧 EnvConfig initialisé: ${this.remoteContentUrl}`, 'INFO');
} else {
console.log('🔧 EnvConfig initialisé:', this.remoteContentUrl);
}
}
buildContentUrl() {
const endpoint = this.config.DO_ENDPOINT.replace(/\/$/, ''); // Supprimer / final si présent
const path = this.config.DO_CONTENT_PATH.replace(/^\//, ''); // Supprimer / initial si présent
return `${endpoint}/${path}/`;
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
// Rebuilder l'URL si changement d'endpoint ou path
if (key === 'DO_ENDPOINT' || key === 'DO_CONTENT_PATH') {
this.remoteContentUrl = this.buildContentUrl();
}
}
// Méthodes utilitaires
isRemoteContentEnabled() {
return this.config.USE_REMOTE_CONTENT;
}
isFallbackEnabled() {
return this.config.FALLBACK_TO_LOCAL;
}
isDebugMode() {
return this.config.DEBUG_MODE;
}
shouldLogContentLoading() {
return this.config.LOG_CONTENT_LOADING;
}
getRemoteContentUrl() {
return this.remoteContentUrl;
}
// Configuration dynamique depuis l'interface
updateRemoteConfig(endpoint, path) {
this.set('DO_ENDPOINT', endpoint);
this.set('DO_CONTENT_PATH', path);
logSh(`🔄 Configuration distante mise à jour: ${this.remoteContentUrl}`, 'INFO');
}
// Méthode pour tester la connectivité
async testRemoteConnection() {
try {
// Test simple avec un fichier qui devrait exister
const testUrl = `${this.remoteContentUrl}test.json`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.REMOTE_TIMEOUT);
const authHeaders = await this.getAuthHeaders('HEAD', testUrl);
const response = await fetch(testUrl, {
method: 'HEAD',
signal: controller.signal,
headers: authHeaders
});
clearTimeout(timeoutId);
return {
success: response.ok || response.status === 403, // 403 = connexion OK mais privé
status: response.status,
url: testUrl,
isPrivate: response.status === 403
};
} catch (error) {
return {
success: false,
error: error.message,
url: `${this.remoteContentUrl}test.json`,
isTimeout: error.name === 'AbortError'
};
}
}
// Génère les headers d'authentification AWS Signature V4 pour DigitalOcean Spaces
async getAuthHeaders(method = 'HEAD', url = '') {
const headers = {};
if (this.config.DO_ACCESS_KEY && this.config.DO_SECRET_KEY) {
try {
const authHeaders = await this.generateAWSSignature(method, url);
Object.assign(headers, authHeaders);
if (typeof logSh !== 'undefined') {
logSh('🔐 Headers d\'authentification DigitalOcean générés', 'DEBUG');
}
} catch (error) {
if (typeof logSh !== 'undefined') {
logSh(`⚠️ Erreur génération signature AWS: ${error.message}`, 'ERROR');
} else {
console.warn('⚠️ Erreur génération signature AWS:', error.message);
}
}
}
return headers;
}
// Implémentation AWS Signature V4 pour DigitalOcean Spaces
async generateAWSSignature(method, url) {
const accessKey = this.config.DO_ACCESS_KEY;
const secretKey = this.config.DO_SECRET_KEY;
const region = this.config.DO_REGION;
const service = 's3';
const now = new Date();
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
const timeStamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '') + 'Z';
// Parse URL
const urlObj = new URL(url || this.remoteContentUrl);
const host = urlObj.hostname;
const canonicalUri = urlObj.pathname || '/';
const canonicalQueryString = urlObj.search ? urlObj.search.slice(1) : '';
// Canonical headers
const canonicalHeaders = `host:${host}\nx-amz-date:${timeStamp}\n`;
const signedHeaders = 'host;x-amz-date';
// Create canonical request
const payloadHash = 'UNSIGNED-PAYLOAD'; // Pour HEAD requests
const canonicalRequest = [
method,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
payloadHash
].join('\n');
// Create string to sign
const algorithm = 'AWS4-HMAC-SHA256';
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
const canonicalRequestHash = await this.sha256(canonicalRequest);
const stringToSign = [
algorithm,
timeStamp,
credentialScope,
canonicalRequestHash
].join('\n');
// Calculate signature
const signingKey = await this.getSigningKey(secretKey, dateStamp, region, service);
const signatureBytes = await this.hmacSha256(signingKey, stringToSign);
const signature = this.uint8ArrayToHex(signatureBytes);
// Create authorization header
const authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return {
'Authorization': authorization,
'X-Amz-Date': timeStamp,
'X-Amz-Content-Sha256': payloadHash
};
}
// Utilitaires cryptographiques avec crypto.subtle (vraie implémentation)
async sha256(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async hmacSha256(key, message) {
const encoder = new TextEncoder();
// Si key est une string, l'encoder
let keyData;
if (typeof key === 'string') {
keyData = encoder.encode(key);
} else {
keyData = key;
}
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
return new Uint8Array(signature);
}
async getSigningKey(secretKey, dateStamp, region, service) {
const encoder = new TextEncoder();
let key = encoder.encode('AWS4' + secretKey);
key = await this.hmacSha256(key, dateStamp);
key = await this.hmacSha256(key, region);
key = await this.hmacSha256(key, service);
key = await this.hmacSha256(key, 'aws4_request');
return key;
}
// Convertir Uint8Array en hex string
uint8ArrayToHex(uint8Array) {
return Array.from(uint8Array).map(b => b.toString(16).padStart(2, '0')).join('');
}
// Diagnostic complet
getDiagnostics() {
return {
remoteContentUrl: this.remoteContentUrl,
remoteEnabled: this.isRemoteContentEnabled(),
fallbackEnabled: this.isFallbackEnabled(),
debugMode: this.isDebugMode(),
endpoint: this.config.DO_ENDPOINT,
contentPath: this.config.DO_CONTENT_PATH,
timestamp: new Date().toISOString()
};
}
}
// Export global
window.EnvConfig = EnvConfig;
// Instance globale
window.envConfig = new EnvConfig();
// Export Node.js (optionnel)
if (typeof module !== 'undefined' && module.exports) {
module.exports = EnvConfig;
}

View File

@ -3,6 +3,7 @@
const GameLoader = {
currentGame: null,
contentScanner: new ContentScanner(),
jsonLoader: new JSONContentLoader(),
loadedModules: {
games: {},
content: {}
@ -23,7 +24,7 @@ const GameLoader = {
this.initGame(gameType, gameModule, contentModule);
} catch (error) {
console.error('Erreur lors du chargement du jeu:', error);
logSh('Erreur lors du chargement du jeu:', error, 'ERROR');
throw error;
}
},
@ -50,7 +51,7 @@ const GameLoader = {
return module;
} catch (error) {
console.error(`Erreur chargement module jeu ${gameType}:`, error);
logSh(`Erreur chargement module jeu ${gameType}:`, error, 'ERROR');
throw error;
}
},
@ -88,7 +89,7 @@ const GameLoader = {
return enrichedContent;
} catch (error) {
console.error(`Erreur chargement contenu ${contentType}:`, error);
logSh(`Erreur chargement contenu ${contentType}:`, error, 'ERROR');
throw error;
}
},
@ -115,8 +116,8 @@ const GameLoader = {
const gameTitle = document.getElementById('game-title');
const scoreDisplay = document.getElementById('current-score');
// Le contenu est déjà enrichi par le ContentScanner, pas besoin d'adaptation supplémentaire
const adaptedContent = contentData;
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
const adaptedContent = this.jsonLoader.loadContent(contentData);
// Mise à jour du titre
const contentName = adaptedContent.name || contentType;
@ -268,7 +269,6 @@ const GameLoader = {
'whack-a-mole-hard': 'WhackAMoleHard',
'memory-match': 'MemoryMatch',
'quiz-game': 'QuizGame',
'temp-games': 'TempGames',
'fill-the-blank': 'FillTheBlank',
'text-reader': 'TextReader',
'adventure-reader': 'AdventureReader',
@ -299,7 +299,6 @@ const GameLoader = {
'whack-a-mole-hard': 'Whack-a-Mole Hard',
'memory-match': 'Memory Match',
'quiz-game': 'Quiz Game',
'temp-games': 'Mini-Jeux',
'fill-the-blank': 'Fill the Blank',
'text-reader': 'Text Reader',
'adventure-reader': 'Adventure Reader'
@ -328,9 +327,9 @@ const GameLoader = {
try {
const audio = new Audio(`assets/sounds/${soundFile}`);
audio.volume = 0.5;
audio.play().catch(e => console.warn('Cannot play sound:', e));
audio.play().catch(e => logSh('Cannot play sound:', e, 'WARN'););
} catch (error) {
console.warn('Sound error:', error);
logSh('Sound error:', error, 'WARN');
}
}
}

View File

@ -0,0 +1,391 @@
// === JSON CONTENT LOADER ===
// Transforme les contenus JSON en format compatible avec les jeux existants
class JSONContentLoader {
constructor() {
this.loadedContent = new Map();
}
/**
* Charge et adapte un contenu JSON pour les jeux
* @param {Object} jsonContent - Le contenu JSON brut
* @returns {Object} - Contenu adapté au format legacy
*/
adapt(jsonContent) {
if (!jsonContent) {
logSh('⚠️ JSONContentLoader - Contenu vide reçu', 'WARN');
return this.createEmptyContent();
}
logSh(`🔄 JSONContentLoader - Adaptation du contenu: ${jsonContent.name || 'Sans nom'}`, 'INFO');
// Créer l'objet de base au format legacy
const adaptedContent = {
// Métadonnées (préservées mais adaptées)
name: jsonContent.name || 'Contenu Sans Nom',
description: jsonContent.description || '',
difficulty: jsonContent.difficulty || 'medium',
language: jsonContent.language || 'english',
icon: jsonContent.icon || '📝',
// === VOCABULAIRE ===
vocabulary: this.adaptVocabulary(jsonContent.vocabulary),
// === PHRASES ET SENTENCES ===
sentences: this.adaptSentences(jsonContent.sentences),
// === TEXTES ===
texts: this.adaptTexts(jsonContent.texts),
// === DIALOGUES ===
dialogues: this.adaptDialogues(jsonContent.dialogues),
// === GRAMMAIRE ===
grammar: this.adaptGrammar(jsonContent.grammar),
// === AUDIO ===
audio: this.adaptAudio(jsonContent.audio),
// === POÈMES ET CULTURE ===
poems: this.adaptPoems(jsonContent.poems),
// === EXERCICES AVANCÉS ===
fillInBlanks: this.adaptFillInBlanks(jsonContent.fillInBlanks),
corrections: this.adaptCorrections(jsonContent.corrections),
comprehension: this.adaptComprehension(jsonContent.comprehension),
matching: this.adaptMatching(jsonContent.matching),
// === EXERCICES GÉNÉRIQUES ===
exercises: this.adaptExercises(jsonContent.exercises),
// === LISTENING (format SBS) ===
listening: this.adaptListening(jsonContent.listening),
// === MÉTADONNÉES DE COMPATIBILITÉ ===
_adapted: true,
_source: 'json',
_adaptedAt: new Date().toISOString()
};
// Nettoyage des valeurs undefined
this.cleanUndefinedValues(adaptedContent);
logSh(`✅ JSONContentLoader - Contenu adapté avec succès`, 'INFO');
logSh(`📊 Stats: ${Object.keys(adaptedContent.vocabulary || {}).length} mots, ${(adaptedContent.sentences || []).length} phrases`, 'INFO');
return adaptedContent;
}
/**
* Adapte le vocabulaire (format JSON Legacy)
*/
adaptVocabulary(vocabulary) {
if (!vocabulary || typeof vocabulary !== 'object') {
return {};
}
const adapted = {};
for (const [word, definition] of Object.entries(vocabulary)) {
if (typeof definition === 'string') {
// Format simple: "cat": "chat"
adapted[word] = definition;
} else if (typeof definition === 'object') {
// Format enrichi: "cat": { translation: "chat", type: "noun", ... }
adapted[word] = {
translation: definition.translation || definition.french || definition.chinese || '',
english: word,
type: definition.type || 'word',
pronunciation: definition.pronunciation || definition.prononciation,
difficulty: definition.difficulty,
examples: definition.examples,
grammarNotes: definition.grammarNotes,
// Compatibilité avec anciens formats
french: definition.french || definition.translation,
chinese: definition.chinese || definition.translation,
image: definition.image,
audio: definition.audio || definition.pronunciation
};
}
}
return adapted;
}
/**
* Adapte les phrases
*/
adaptSentences(sentences) {
if (!Array.isArray(sentences)) {
return [];
}
return sentences.map(sentence => {
if (typeof sentence === 'string') {
return { english: sentence, translation: '' };
}
return {
english: sentence.english || '',
french: sentence.french || sentence.translation,
chinese: sentence.chinese || sentence.translation,
translation: sentence.translation || sentence.french || sentence.chinese,
prononciation: sentence.prononciation || sentence.pinyin,
audio: sentence.audio,
difficulty: sentence.difficulty
};
});
}
/**
* Adapte les textes
*/
adaptTexts(texts) {
if (!Array.isArray(texts)) {
return [];
}
return texts.map(text => ({
title: text.title || 'Sans titre',
content: text.content || '',
translation: text.translation || '',
french: text.french || text.translation,
chinese: text.chinese || text.translation,
audio: text.audio,
difficulty: text.difficulty
}));
}
/**
* Adapte les dialogues
*/
adaptDialogues(dialogues) {
if (!Array.isArray(dialogues)) {
return [];
}
return dialogues.map(dialogue => ({
title: dialogue.title || 'Dialogue',
conversation: Array.isArray(dialogue.conversation)
? dialogue.conversation.map(line => ({
speaker: line.speaker || 'Speaker',
english: line.english || '',
french: line.french || line.translation,
chinese: line.chinese || line.translation,
translation: line.translation || line.french || line.chinese,
prononciation: line.prononciation || line.pinyin,
audio: line.audio
}))
: []
}));
}
/**
* Adapte la grammaire
*/
adaptGrammar(grammar) {
if (!grammar || typeof grammar !== 'object') {
return {};
}
// La grammaire reste en format objet (compatible)
return grammar;
}
/**
* Adapte le contenu audio
*/
adaptAudio(audio) {
if (!audio || typeof audio !== 'object') {
return {};
}
return {
withText: Array.isArray(audio.withText) ? audio.withText : [],
withoutText: Array.isArray(audio.withoutText) ? audio.withoutText : []
};
}
/**
* Adapte les poèmes
*/
adaptPoems(poems) {
if (!Array.isArray(poems)) {
return [];
}
return poems.map(poem => ({
title: poem.title || 'Poème',
content: poem.content || '',
translation: poem.translation || '',
audioFile: poem.audioFile,
culturalContext: poem.culturalContext
}));
}
/**
* Adapte les exercices Fill-in-the-Blanks
*/
adaptFillInBlanks(fillInBlanks) {
if (!Array.isArray(fillInBlanks)) {
return [];
}
return fillInBlanks;
}
/**
* Adapte les exercices de correction
*/
adaptCorrections(corrections) {
if (!Array.isArray(corrections)) {
return [];
}
return corrections;
}
/**
* Adapte les exercices de compréhension
*/
adaptComprehension(comprehension) {
if (!Array.isArray(comprehension)) {
return [];
}
return comprehension;
}
/**
* Adapte les exercices de matching
*/
adaptMatching(matching) {
if (!Array.isArray(matching)) {
return [];
}
return matching;
}
/**
* Adapte les exercices génériques
*/
adaptExercises(exercises) {
if (!exercises || typeof exercises !== 'object') {
return {};
}
return exercises;
}
/**
* Adapte le contenu listening (format SBS)
*/
adaptListening(listening) {
if (!listening || typeof listening !== 'object') {
return {};
}
return listening;
}
/**
* Nettoie les valeurs undefined de l'objet
*/
cleanUndefinedValues(obj) {
Object.keys(obj).forEach(key => {
if (obj[key] === undefined) {
delete obj[key];
} else if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
this.cleanUndefinedValues(obj[key]);
}
});
}
/**
* Crée un contenu vide par défaut
*/
createEmptyContent() {
return {
name: 'Contenu Vide',
description: 'Aucun contenu disponible',
difficulty: 'easy',
vocabulary: {},
sentences: [],
texts: [],
dialogues: [],
grammar: {},
exercises: {},
_adapted: true,
_source: 'empty',
_adaptedAt: new Date().toISOString()
};
}
/**
* Analyse la richesse du contenu et génère des statistiques
*/
analyzeContent(adaptedContent) {
const stats = {
vocabularyCount: Object.keys(adaptedContent.vocabulary || {}).length,
sentenceCount: (adaptedContent.sentences || []).length,
textCount: (adaptedContent.texts || []).length,
dialogueCount: (adaptedContent.dialogues || []).length,
grammarTopics: Object.keys(adaptedContent.grammar || {}).length,
audioContent: !!(adaptedContent.audio && (adaptedContent.audio.withText || adaptedContent.audio.withoutText)),
poemCount: (adaptedContent.poems || []).length,
exerciseTypes: Object.keys(adaptedContent.exercises || {}).length,
totalItems: 0
};
stats.totalItems = stats.vocabularyCount + stats.sentenceCount +
stats.textCount + stats.dialogueCount + stats.poemCount;
stats.richness = stats.totalItems > 50 ? 'rich' :
stats.totalItems > 20 ? 'medium' : 'basic';
return stats;
}
/**
* Teste si un objet est au format JSON (vs legacy JS)
*/
isJSONFormat(content) {
return content && (
content.hasOwnProperty('name') ||
content.hasOwnProperty('description') ||
content.hasOwnProperty('language') ||
content._adapted === true
);
}
/**
* Point d'entrée principal - décide s'il faut adapter ou pas
*/
loadContent(content) {
if (!content) {
return this.createEmptyContent();
}
// Si déjà adapté, retourner tel quel
if (content._adapted === true) {
return content;
}
// Si format JSON, adapter
if (this.isJSONFormat(content)) {
return this.adapt(content);
}
// Sinon, c'est du format legacy, retourner tel quel
return content;
}
}
// Export global
window.JSONContentLoader = JSONContentLoader;
// Export Node.js (optionnel)
if (typeof module !== 'undefined' && module.exports) {
module.exports = JSONContentLoader;
}

View File

@ -16,17 +16,17 @@ const AppNavigation = {
async loadGamesConfig() {
// Utilisation directe de la config par défaut (pas de fetch)
console.log('📁 Utilisation de la configuration par défaut');
logSh('📁 Utilisation de la configuration par défaut', 'INFO');
this.gamesConfig = this.getDefaultConfig();
},
async initContentScanner() {
try {
console.log('🔍 Initialisation du scanner de contenu...');
logSh('🔍 Initialisation du scanner de contenu...', 'INFO');
this.scannedContent = await this.contentScanner.scanAllContent();
console.log(`${this.scannedContent.found.length} modules de contenu détectés automatiquement`);
logSh(`${this.scannedContent.found.length} modules de contenu détectés automatiquement`, 'INFO');
} catch (error) {
console.error('Erreur scan contenu:', error);
logSh('Erreur scan contenu:', error, 'ERROR');
}
},
@ -57,12 +57,6 @@ const AppNavigation = {
icon: '❓',
description: 'Answer vocabulary questions!'
},
'temp-games': {
enabled: true,
name: 'Jeux Temporaires',
icon: '🎯',
description: 'Mini-jeux en développement'
},
'fill-the-blank': {
enabled: true,
name: 'Fill the Blank',
@ -177,6 +171,7 @@ const AppNavigation = {
// Navigation vers une page
navigateTo(page, game = null, content = null) {
logSh(`🧭 Navigation vers: ${page} ${game ? `(jeu: ${game})` : ''} ${content ? `(contenu: ${content})` : ''}`, 'INFO');
const params = { page };
if (game) params.game = game;
if (content) params.content = content;
@ -210,9 +205,11 @@ const AppNavigation = {
// Retour en arrière
goBack() {
logSh(`⬅️ Retour en arrière depuis: ${this.currentPage}`, 'INFO');
if (this.navigationHistory.length > 1) {
this.navigationHistory.pop(); // Retirer la page actuelle
const previousPage = this.navigationHistory[this.navigationHistory.length - 1];
logSh(`📍 Retour vers: ${previousPage}`, 'DEBUG');
const params = Utils.getUrlParams();
@ -228,21 +225,26 @@ const AppNavigation = {
// Affichage page d'accueil
showHomePage() {
logSh('🏠 Affichage page d\'accueil', 'INFO');
this.hideAllPages();
document.getElementById('home-page').classList.add('active');
this.currentPage = 'home';
this.updateBreadcrumb();
},
// Affichage page sélection jeux
showGamesPage() {
logSh('🎮 Affichage page sélection des jeux', 'INFO');
this.hideAllPages();
document.getElementById('games-page').classList.add('active');
this.renderGamesGrid();
this.currentPage = 'games';
this.updateBreadcrumb();
},
// Affichage page sélection niveaux
showLevelsPage(gameType) {
logSh(`📚 Affichage page sélection des niveaux pour: ${gameType}`, 'INFO');
this.hideAllPages();
document.getElementById('levels-page').classList.add('active');
this.renderLevelsGrid(gameType);
@ -251,9 +253,12 @@ const AppNavigation = {
// Mise à jour de la description
const gameInfo = this.gamesConfig?.games[gameType];
if (gameInfo) {
logSh(`🎯 Description mise à jour: ${gameInfo.name}`, 'DEBUG');
document.getElementById('level-description').textContent =
`Sélectionne le contenu pour jouer à ${gameInfo.name}`;
}
this.updateBreadcrumb();
},
// Affichage page de jeu
@ -266,8 +271,9 @@ const AppNavigation = {
try {
await GameLoader.loadGame(gameType, contentType);
this.updateBreadcrumb();
} catch (error) {
console.error('Erreur chargement jeu:', error);
logSh('Erreur chargement jeu:', error, 'ERROR');
Utils.showToast('Erreur lors du chargement du jeu', 'error');
this.goBack();
} finally {
@ -284,16 +290,22 @@ const AppNavigation = {
// Rendu grille des jeux
renderGamesGrid() {
logSh('🎲 Génération de la grille des jeux...', 'DEBUG');
const grid = document.getElementById('games-grid');
grid.innerHTML = '';
if (!this.gamesConfig) return;
if (!this.gamesConfig) {
logSh('❌ Pas de configuration de jeux disponible', 'ERROR');
return;
}
Object.entries(this.gamesConfig.games).forEach(([key, game]) => {
if (game.enabled) {
const card = this.createGameCard(key, game);
grid.appendChild(card);
}
const enabledGames = Object.entries(this.gamesConfig.games).filter(([key, game]) => game.enabled);
logSh(`🎯 ${enabledGames.length} jeux activés trouvés`, 'INFO');
enabledGames.forEach(([key, game]) => {
logSh(` Ajout de la carte: ${game.name}`, 'DEBUG');
const card = this.createGameCard(key, game);
grid.appendChild(card);
});
},
@ -308,6 +320,7 @@ const AppNavigation = {
`;
card.addEventListener('click', () => {
logSh(`🎮 Clic sur la carte du jeu: ${gameInfo.name} (${gameKey})`, 'INFO');
Utils.animateElement(card, 'pulse');
this.navigateTo('levels', gameKey);
});
@ -336,7 +349,7 @@ const AppNavigation = {
const compatibleContent = await this.contentScanner.getContentByGame(gameType);
const contentToShow = compatibleContent.length > 0 ? compatibleContent : availableContent;
console.log(`📋 Affichage de ${contentToShow.length} modules pour ${gameType}`);
logSh(`📋 Affichage de ${contentToShow.length} modules pour ${gameType}`, 'INFO');
// Créer les cartes pour chaque contenu trouvé
contentToShow.forEach(content => {
@ -358,7 +371,7 @@ const AppNavigation = {
}
} catch (error) {
console.error('Erreur rendu levels:', error);
logSh('Erreur rendu levels:', error, 'ERROR');
grid.innerHTML = '<div class="error-content">❌ Erreur lors du chargement du contenu</div>';
}
},
@ -418,6 +431,7 @@ const AppNavigation = {
`;
card.addEventListener('click', () => {
logSh(`📚 Clic sur la carte du contenu: ${contentInfo.name} (${contentKey}) pour le jeu ${gameType}`, 'INFO');
Utils.animateElement(card, 'pulse');
this.navigateTo('play', gameType, contentKey);
});
@ -427,6 +441,7 @@ const AppNavigation = {
// Mise à jour du breadcrumb
updateBreadcrumb() {
logSh(`🍞 Mise à jour du breadcrumb pour page: ${this.currentPage}`, 'DEBUG');
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = '';
@ -471,6 +486,7 @@ const AppNavigation = {
if (!isActive) {
item.addEventListener('click', () => {
logSh(`🍞 Clic sur breadcrumb: ${text}${page}`, 'INFO');
const params = Utils.getUrlParams();
if (page === 'home') {

117
js/core/simple-logger.js Normal file
View File

@ -0,0 +1,117 @@
// === SIMPLE LOGGER - VERSION DE SECOURS ===
// Version simplifiée qui marche à coup sûr
console.log('🔧 Simple Logger chargé');
// Logger global simple
window.logEntries = [];
window.logSh = function(message, level = 'INFO') {
const timestamp = new Date().toLocaleTimeString('fr-FR');
const entry = { timestamp, level, message };
// Stocker
window.logEntries.push(entry);
if (window.logEntries.length > 200) {
window.logEntries.shift();
}
// Console
const color = level === 'ERROR' ? 'color: red' :
level === 'WARN' ? 'color: orange' :
level === 'DEBUG' ? 'color: blue' : 'color: green';
console.log(`%c[${timestamp}] ${level}: ${message}`, color);
// Mettre à jour l'affichage si ouvert
if (window.simpleLoggerVisible) {
updateLogDisplay();
}
};
window.simpleLoggerVisible = false;
window.toggleSimpleLogger = function() {
console.log('🔧 Toggle simple logger');
let container = document.getElementById('simple-logger');
if (!container) {
// Créer le container
container = document.createElement('div');
container.id = 'simple-logger';
container.style.cssText = `
position: fixed;
top: 60px;
right: 10px;
width: 500px;
max-height: 400px;
background: white;
border: 2px solid #007bff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
font-family: monospace;
font-size: 12px;
z-index: 10000;
display: none;
`;
container.innerHTML = `
<div style="background: #007bff; color: white; padding: 10px; display: flex; justify-content: space-between; align-items: center;">
<strong>📋 System Logs</strong>
<button onclick="toggleSimpleLogger()" style="background: rgba(255,255,255,0.2); border: none; color: white; padding: 4px 8px; border-radius: 4px; cursor: pointer;"></button>
</div>
<div id="log-content" style="max-height: 350px; overflow-y: auto; padding: 0;">
</div>
<div style="padding: 8px; background: #f8f9fa; border-top: 1px solid #eee; text-align: center; font-size: 10px; color: #666;">
${window.logEntries.length} logs Ctrl+Shift+L pour toggle
</div>
`;
document.body.appendChild(container);
console.log('✅ Container créé');
}
// Toggle visibility
if (window.simpleLoggerVisible) {
container.style.display = 'none';
window.simpleLoggerVisible = false;
console.log('Logger masqué');
} else {
container.style.display = 'block';
window.simpleLoggerVisible = true;
updateLogDisplay();
console.log('Logger affiché');
}
};
function updateLogDisplay() {
const content = document.getElementById('log-content');
if (!content) return;
const html = window.logEntries.slice(-50).map(entry => {
const levelColor = entry.level === 'ERROR' ? '#dc3545' :
entry.level === 'WARN' ? '#ffc107' :
entry.level === 'DEBUG' ? '#007bff' : '#28a745';
return `
<div style="padding: 4px 8px; border-bottom: 1px solid #eee; display: flex; gap: 8px; font-size: 11px;">
<span style="color: #666; min-width: 60px;">${entry.timestamp}</span>
<span style="color: ${levelColor}; font-weight: bold; min-width: 50px;">${entry.level}</span>
<span style="flex: 1;">${entry.message}</span>
</div>
`;
}).join('');
content.innerHTML = html;
content.scrollTop = content.scrollHeight;
}
// Raccourci clavier
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'L') {
toggleSimpleLogger();
}
});
// Logger disponible
console.log('✅ Simple Logger prêt - window.logSh() disponible');

27
js/core/test-logger.js Normal file
View File

@ -0,0 +1,27 @@
// VERSION TEST ULTRA SIMPLE
console.log('🔧 Test logger chargé');
// Variable globale pour stocker les logs
window.testLogs = [];
// Fonction logSh basique
window.logSh = function(message, level) {
level = level || 'INFO';
const time = new Date().toLocaleTimeString();
// Stocker
window.testLogs.push(time + ' ' + level + ': ' + message);
// Afficher dans console
console.log('[' + time + '] ' + level + ': ' + message);
};
// Fonction toggle basique
window.showTestLogs = function() {
alert('Logs:\n' + window.testLogs.slice(-10).join('\n'));
};
// Test immédiat
window.logSh('Test logger initialisé', 'INFO');
console.log('✅ Test logger prêt');

View File

@ -133,7 +133,7 @@ const Utils = {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn('LocalStorage not available:', e);
logSh('LocalStorage not available:', e, 'WARN');
}
},
@ -142,7 +142,7 @@ const Utils = {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.warn('LocalStorage read error:', e);
logSh('LocalStorage read error:', e, 'WARN');
return defaultValue;
}
},
@ -151,7 +151,7 @@ const Utils = {
try {
localStorage.removeItem(key);
} catch (e) {
console.warn('LocalStorage remove error:', e);
logSh('LocalStorage remove error:', e, 'WARN');
}
}
}

View File

@ -0,0 +1,99 @@
// === WEBSOCKET LOGGER ===
// Envoie les logs vers le serveur WebSocket de ton système
console.log('🔧 WebSocket Logger chargé');
// Variables globales
window.wsLogger = null;
window.logQueue = [];
window.isConnected = false;
// Fonction pour se connecter au serveur WebSocket
function connectToLogServer() {
try {
window.wsLogger = new WebSocket('ws://localhost:8082');
window.wsLogger.onopen = function() {
console.log('✅ Connecté au serveur de logs WebSocket');
window.isConnected = true;
// Vider la queue des logs en attente
while (window.logQueue.length > 0) {
const logData = window.logQueue.shift();
window.wsLogger.send(JSON.stringify(logData));
}
};
window.wsLogger.onclose = function() {
console.log('❌ Connexion WebSocket fermée');
window.isConnected = false;
// Tentative de reconnexion après 3 secondes
setTimeout(connectToLogServer, 3000);
};
window.wsLogger.onerror = function(error) {
console.log('❌ Erreur WebSocket:', error);
window.isConnected = false;
};
} catch (error) {
console.log('❌ Impossible de se connecter au serveur de logs:', error);
setTimeout(connectToLogServer, 5000);
}
}
// Fonction logSh qui envoie au WebSocket
window.logSh = function(message, level = 'INFO') {
const timestamp = new Date().toISOString();
const logData = {
timestamp: timestamp,
level: level.toUpperCase(),
message: message
};
// Log aussi dans la console pour backup
const color = level === 'ERROR' ? 'color: red' :
level === 'WARN' ? 'color: orange' :
level === 'DEBUG' ? 'color: blue' : 'color: green';
console.log(`%c[${new Date().toLocaleTimeString()}] ${level}: ${message}`, color);
// Envoyer au WebSocket
if (window.isConnected && window.wsLogger) {
try {
window.wsLogger.send(JSON.stringify(logData));
} catch (error) {
// Si erreur d'envoi, mettre en queue
window.logQueue.push(logData);
}
} else {
// Pas connecté, mettre en queue
window.logQueue.push(logData);
// Limiter la queue à 100 messages
if (window.logQueue.length > 100) {
window.logQueue.shift();
}
}
};
// Fonctions de convenance
window.logTrace = function(message) { window.logSh(message, 'TRACE'); };
window.logDebug = function(message) { window.logSh(message, 'DEBUG'); };
window.logInfo = function(message) { window.logSh(message, 'INFO'); };
window.logWarn = function(message) { window.logSh(message, 'WARN'); };
window.logError = function(message) { window.logSh(message, 'ERROR'); };
// Bouton pour ouvrir l'interface de logs
window.openLogsInterface = function() {
const logsUrl = 'http://localhost:8000/export_logger/logs-viewer.html';
window.open(logsUrl, 'LogsViewer', 'width=1200,height=800,scrollbars=yes,resizable=yes');
};
// Initialisation
connectToLogServer();
// Test initial
window.logSh('🚀 WebSocket Logger initialisé', 'INFO');
console.log('✅ WebSocket Logger prêt - logs envoyés vers ws://localhost:8082');

View File

@ -33,7 +33,7 @@ class AdventureReaderGame {
init() {
if ((!this.vocabulary || this.vocabulary.length === 0) &&
(!this.sentences || this.sentences.length === 0)) {
console.error('No content available for Adventure Reader');
logSh('No content available for Adventure Reader', 'ERROR');
this.showInitError();
return;
}
@ -909,12 +909,12 @@ class AdventureReaderGame {
}
start() {
console.log('⚔️ Adventure Reader: Starting');
logSh('⚔️ Adventure Reader: Starting', 'INFO');
document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!';
}
restart() {
console.log('🔄 Adventure Reader: Restarting');
logSh('🔄 Adventure Reader: Restarting', 'INFO');
this.reset();
this.start();
}

View File

@ -257,7 +257,7 @@ class ChineseStudyGame {
start() {
this.isRunning = true;
console.log('Chinese Study Mode initialized');
logSh('Chinese Study Mode initialized', 'INFO');
}
destroy() {

View File

@ -25,7 +25,7 @@ class FillTheBlankGame {
init() {
// Vérifier que nous avons des phrases
if (!this.sentences || this.sentences.length === 0) {
console.error('Aucune phrase disponible pour Fill the Blank');
logSh('Aucune phrase disponible pour Fill the Blank', 'ERROR');
this.showInitError();
return;
}
@ -49,24 +49,24 @@ class FillTheBlankGame {
extractSentences(content) {
let sentences = [];
console.log('🔍 Extraction phrases depuis:', content?.name || 'contenu');
logSh('🔍 Extraction phrases depuis:', content?.name || 'contenu', 'INFO');
// Utiliser le contenu brut du module si disponible
if (content.rawContent) {
console.log('📦 Utilisation du contenu brut du module');
logSh('📦 Utilisation du contenu brut du module', 'INFO');
return this.extractSentencesFromRaw(content.rawContent);
}
// Format avec sentences array
if (content.sentences && Array.isArray(content.sentences)) {
console.log('📝 Format sentences détecté');
logSh('📝 Format sentences détecté', 'INFO');
sentences = content.sentences.filter(sentence =>
sentence.english && sentence.english.trim() !== ''
);
}
// Format moderne avec contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 Format contentItems détecté');
logSh('🆕 Format contentItems détecté', 'INFO');
sentences = content.contentItems
.filter(item => item.type === 'sentence' && item.english)
.map(item => ({
@ -80,7 +80,7 @@ class FillTheBlankGame {
}
extractSentencesFromRaw(rawContent) {
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
logSh('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module', 'INFO');
let sentences = [];
// Format simple (sentences array)
@ -88,7 +88,7 @@ class FillTheBlankGame {
sentences = rawContent.sentences.filter(sentence =>
sentence.english && sentence.english.trim() !== ''
);
console.log(`📝 ${sentences.length} phrases extraites depuis sentences array`);
logSh(`📝 ${sentences.length} phrases extraites depuis sentences array`, 'INFO');
}
// Format contentItems
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
@ -99,7 +99,7 @@ class FillTheBlankGame {
french: item.french || item.translation,
chinese: item.chinese
}));
console.log(`🆕 ${sentences.length} phrases extraites depuis contentItems`);
logSh(`🆕 ${sentences.length} phrases extraites depuis contentItems`, 'INFO');
}
return this.finalizeSentences(sentences);
@ -115,20 +115,20 @@ class FillTheBlankGame {
);
if (sentences.length === 0) {
console.error('❌ Aucune phrase valide trouvée');
logSh('❌ Aucune phrase valide trouvée', 'ERROR');
// Phrases de démonstration en dernier recours
sentences = [
{ english: "I am learning English.", chinese: "我正在学英语。" },
{ english: "She goes to school every day.", chinese: "她每天都去学校。" },
{ english: "We like to play games together.", chinese: "我们喜欢一起玩游戏。" }
];
console.warn('🚨 Utilisation de phrases de démonstration');
logSh('🚨 Utilisation de phrases de démonstration', 'WARN');
}
// Mélanger les phrases
sentences = this.shuffleArray(sentences);
console.log(`✅ Fill the Blank: ${sentences.length} phrases finalisées`);
logSh(`✅ Fill the Blank: ${sentences.length} phrases finalisées`, 'INFO');
return sentences;
}
@ -199,12 +199,12 @@ class FillTheBlankGame {
}
start() {
console.log('🎮 Fill the Blank: Démarrage du jeu');
logSh('🎮 Fill the Blank: Démarrage du jeu', 'INFO');
this.loadNextSentence();
}
restart() {
console.log('🔄 Fill the Blank: Redémarrage du jeu');
logSh('🔄 Fill the Blank: Redémarrage du jeu', 'INFO');
this.reset();
this.start();
}

View File

@ -25,7 +25,7 @@ class MemoryMatchGame {
init() {
// Check if we have enough vocabulary
if (!this.vocabulary || this.vocabulary.length < this.totalPairs) {
console.error('Not enough vocabulary for Memory Match');
logSh('Not enough vocabulary for Memory Match', 'ERROR');
this.showInitError();
return;
}
@ -49,17 +49,17 @@ class MemoryMatchGame {
extractVocabulary(content) {
let vocabulary = [];
console.log('📝 Extracting vocabulary from:', content?.name || 'content');
logSh('📝 Extracting vocabulary from:', content?.name || 'content', 'INFO');
// Use raw module content if available
if (content.rawContent) {
console.log('📦 Using raw module content');
logSh('📦 Using raw module content', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Modern format with contentItems
if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 ContentItems format detected');
logSh('🆕 ContentItems format detected', 'INFO');
const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary');
if (vocabItems.length > 0) {
vocabulary = vocabItems[0].items || [];
@ -67,7 +67,7 @@ class MemoryMatchGame {
}
// Legacy format with vocabulary array
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
console.log('📚 Vocabulary array format detected');
logSh('📚 Vocabulary array format detected', 'INFO');
vocabulary = content.vocabulary;
}
@ -75,7 +75,7 @@ class MemoryMatchGame {
}
extractVocabularyFromRaw(rawContent) {
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Check vocabulary object format (key-value pairs)
@ -84,12 +84,12 @@ class MemoryMatchGame {
english: english,
french: translation
}));
console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`);
logSh(`📝 ${vocabulary.length} vocabulary pairs extracted from object`, 'INFO');
}
// Check vocabulary array format
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary;
console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`);
logSh(`📚 ${vocabulary.length} vocabulary items extracted from array`, 'INFO');
}
return this.finalizeVocabulary(vocabulary);
@ -107,7 +107,7 @@ class MemoryMatchGame {
}));
if (vocabulary.length === 0) {
console.error('❌ No valid vocabulary found');
logSh('❌ No valid vocabulary found', 'ERROR');
// Demo vocabulary as fallback
vocabulary = [
{ english: "cat", french: "chat" },
@ -119,10 +119,10 @@ class MemoryMatchGame {
{ english: "food", french: "nourriture" },
{ english: "friend", french: "ami" }
];
console.warn('🚨 Using demo vocabulary');
logSh('🚨 Using demo vocabulary', 'WARN');
}
console.log(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`);
logSh(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`, 'INFO');
return vocabulary;
}
@ -377,12 +377,12 @@ class MemoryMatchGame {
}
start() {
console.log('🧠 Memory Match: Starting');
logSh('🧠 Memory Match: Starting', 'INFO');
this.showFeedback('Find matching English-French pairs!', 'info');
}
restart() {
console.log('🔄 Memory Match: Restarting');
logSh('🔄 Memory Match: Restarting', 'INFO');
this.reset();
this.start();
}

View File

@ -25,7 +25,7 @@ class QuizGame {
init() {
// Check if we have enough vocabulary
if (!this.vocabulary || this.vocabulary.length < 4) {
console.error('Not enough vocabulary for Quiz Game');
logSh('Not enough vocabulary for Quiz Game', 'ERROR');
this.showInitError();
return;
}
@ -51,17 +51,17 @@ class QuizGame {
extractVocabulary(content) {
let vocabulary = [];
console.log('📝 Extracting vocabulary from:', content?.name || 'content');
logSh('📝 Extracting vocabulary from:', content?.name || 'content', 'INFO');
// Use raw module content if available
if (content.rawContent) {
console.log('📦 Using raw module content');
logSh('📦 Using raw module content', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Modern format with contentItems
if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 ContentItems format detected');
logSh('🆕 ContentItems format detected', 'INFO');
const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary');
if (vocabItems.length > 0) {
vocabulary = vocabItems[0].items || [];
@ -69,7 +69,7 @@ class QuizGame {
}
// Legacy format with vocabulary array
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
console.log('📚 Vocabulary array format detected');
logSh('📚 Vocabulary array format detected', 'INFO');
vocabulary = content.vocabulary;
}
@ -77,7 +77,7 @@ class QuizGame {
}
extractVocabularyFromRaw(rawContent) {
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Check vocabulary object format (key-value pairs)
@ -86,12 +86,12 @@ class QuizGame {
english: english,
french: translation
}));
console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`);
logSh(`📝 ${vocabulary.length} vocabulary pairs extracted from object`, 'INFO');
}
// Check vocabulary array format
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary;
console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`);
logSh(`📚 ${vocabulary.length} vocabulary items extracted from array`, 'INFO');
}
return this.finalizeVocabulary(vocabulary);
@ -109,7 +109,7 @@ class QuizGame {
}));
if (vocabulary.length === 0) {
console.error('❌ No valid vocabulary found');
logSh('❌ No valid vocabulary found', 'ERROR');
// Demo vocabulary as fallback
vocabulary = [
{ english: "cat", french: "chat" },
@ -121,13 +121,13 @@ class QuizGame {
{ english: "food", french: "nourriture" },
{ english: "friend", french: "ami" }
];
console.warn('🚨 Using demo vocabulary');
logSh('🚨 Using demo vocabulary', 'WARN');
}
// Shuffle vocabulary for random questions
vocabulary = vocabulary.sort(() => Math.random() - 0.5);
console.log(`✅ Quiz Game: ${vocabulary.length} vocabulary items finalized`);
logSh(`✅ Quiz Game: ${vocabulary.length} vocabulary items finalized`, 'INFO');
return vocabulary;
}
@ -321,12 +321,12 @@ class QuizGame {
}
start() {
console.log('❓ Quiz Game: Starting');
logSh('❓ Quiz Game: Starting', 'INFO');
this.showFeedback('Choose the correct translation for each word!', 'info');
}
restart() {
console.log('🔄 Quiz Game: Restarting');
logSh('🔄 Quiz Game: Restarting', 'INFO');
this.reset();
this.start();
}

View File

@ -127,7 +127,7 @@ class StoryBuilderGame {
loadStoryContent() {
if (!this.contentEngine) {
console.warn('ContentEngine non disponible, utilisation du contenu de base');
logSh('ContentEngine non disponible, utilisation du contenu de base', 'WARN');
this.setupBasicContent();
return;
}

View File

@ -1,736 +0,0 @@
// === MODULE JEUX TEMPORAIRES ===
class TempGamesModule {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
this.currentGame = null;
this.availableGames = [
{
id: 'word-match',
name: 'Word Match',
icon: '🎯',
description: 'Associe les mots anglais avec leur traduction',
difficulty: 'easy'
},
{
id: 'quick-translation',
name: 'Quick Translation',
icon: '⚡',
description: 'Traduis le mot le plus rapidement possible',
difficulty: 'medium'
},
{
id: 'word-builder',
name: 'Word Builder',
icon: '🔤',
description: 'Reconstitue le mot lettre par lettre',
difficulty: 'medium'
}
];
this.init();
}
init() {
this.showGameSelector();
}
showGameSelector() {
this.container.innerHTML = `
<div class="temp-games-wrapper">
<div class="game-selector-header">
<h3>🎯 Mini-Jeux Temporaires</h3>
<p>Sélectionne un mini-jeu pour t'amuser avec le vocabulaire !</p>
</div>
<div class="mini-games-grid">
${this.availableGames.map(game => this.createGameCard(game)).join('')}
</div>
<div class="temp-games-info">
<p><em>Ces jeux sont en développement et seront bientôt des modules complets !</em></p>
</div>
</div>
`;
this.setupGameSelector();
}
createGameCard(game) {
const difficultyColor = {
easy: '#10B981',
medium: '#F59E0B',
hard: '#EF4444'
}[game.difficulty];
return `
<div class="mini-game-card" data-game="${game.id}">
<div class="mini-game-icon">${game.icon}</div>
<h4 class="mini-game-title">${game.name}</h4>
<p class="mini-game-description">${game.description}</p>
<div class="mini-game-difficulty" style="color: ${difficultyColor}">
${game.difficulty.toUpperCase()}
</div>
<button class="play-mini-game-btn">Jouer</button>
</div>
`;
}
setupGameSelector() {
document.querySelectorAll('.mini-game-card').forEach(card => {
card.addEventListener('click', () => {
const gameId = card.dataset.game;
this.startMiniGame(gameId);
});
});
}
startMiniGame(gameId) {
const game = this.availableGames.find(g => g.id === gameId);
if (!game) return;
switch(gameId) {
case 'word-match':
this.startWordMatch();
break;
case 'quick-translation':
this.startQuickTranslation();
break;
case 'word-builder':
this.startWordBuilder();
break;
}
}
// === WORD MATCH GAME ===
startWordMatch() {
this.container.innerHTML = `
<div class="mini-game word-match-game">
<div class="mini-game-header">
<button class="back-to-selector"> Retour</button>
<h3>🎯 Word Match</h3>
<div class="mini-score">Score: <span id="match-score">0</span></div>
</div>
<div class="word-match-board">
<div class="english-words" id="english-words">
<!-- Mots anglais -->
</div>
<div class="french-words" id="french-words">
<!-- Mots français -->
</div>
</div>
<div class="match-feedback" id="match-feedback">
Clique sur un mot anglais, puis sur sa traduction française !
</div>
</div>
`;
this.setupWordMatch();
}
setupWordMatch() {
document.querySelector('.back-to-selector').addEventListener('click', () => {
this.showGameSelector();
});
const words = this.content.vocabulary.slice(0, 6).map(w => ({
english: w.english,
french: w.french
}));
const shuffledFrench = [...words].sort(() => Math.random() - 0.5);
const englishContainer = document.getElementById('english-words');
const frenchContainer = document.getElementById('french-words');
let selectedEnglish = null;
let matchedPairs = 0;
let score = 0;
words.forEach((word, index) => {
const englishBtn = document.createElement('button');
englishBtn.className = 'word-btn english-btn';
englishBtn.textContent = word.english;
englishBtn.dataset.word = word.english;
englishContainer.appendChild(englishBtn);
englishBtn.addEventListener('click', () => {
document.querySelectorAll('.english-btn').forEach(btn =>
btn.classList.remove('selected'));
englishBtn.classList.add('selected');
selectedEnglish = word.english;
});
});
shuffledFrench.forEach(word => {
const frenchBtn = document.createElement('button');
frenchBtn.className = 'word-btn french-btn';
frenchBtn.textContent = word.french;
frenchBtn.dataset.word = word.french;
frenchContainer.appendChild(frenchBtn);
frenchBtn.addEventListener('click', () => {
if (!selectedEnglish) {
document.getElementById('match-feedback').textContent =
'Sélectionne d\'abord un mot anglais !';
return;
}
const correctWord = words.find(w => w.english === selectedEnglish);
if (correctWord && correctWord.french === word.french) {
// Correct match
score += 10;
matchedPairs++;
document.querySelector(`[data-word="${selectedEnglish}"]`).classList.add('matched');
frenchBtn.classList.add('matched');
document.getElementById('match-feedback').textContent = 'Parfait ! 🎉';
if (matchedPairs === words.length) {
setTimeout(() => {
alert(`Félicitations ! Score final: ${score}`);
this.onGameEnd(score);
}, 1000);
}
} else {
// Wrong match
score = Math.max(0, score - 2);
document.getElementById('match-feedback').textContent =
`Non, "${selectedEnglish}" ne correspond pas à "${word.french}"`;
}
document.getElementById('match-score').textContent = score;
this.onScoreUpdate(score);
selectedEnglish = null;
document.querySelectorAll('.english-btn').forEach(btn =>
btn.classList.remove('selected'));
});
});
}
// === QUICK TRANSLATION GAME ===
startQuickTranslation() {
this.container.innerHTML = `
<div class="mini-game quick-translation-game">
<div class="mini-game-header">
<button class="back-to-selector"> Retour</button>
<h3> Quick Translation</h3>
<div class="game-stats">
<span>Score: <span id="quick-score">0</span></span>
<span>Temps: <span id="quick-time">30</span>s</span>
</div>
</div>
<div class="quick-translation-board">
<div class="word-to-translate">
<h2 id="current-word">---</h2>
<p>Traduction en français :</p>
</div>
<div class="translation-options" id="translation-options">
<!-- Options de traduction -->
</div>
</div>
<button id="start-quick-game" class="start-btn">🎮 Commencer</button>
</div>
`;
this.setupQuickTranslation();
}
setupQuickTranslation() {
document.querySelector('.back-to-selector').addEventListener('click', () => {
this.showGameSelector();
});
let currentWordIndex = 0;
let score = 0;
let timeLeft = 30;
let gameTimer = null;
let isPlaying = false;
const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 10);
document.getElementById('start-quick-game').addEventListener('click', () => {
if (isPlaying) return;
isPlaying = true;
currentWordIndex = 0;
score = 0;
timeLeft = 30;
document.getElementById('start-quick-game').style.display = 'none';
gameTimer = setInterval(() => {
timeLeft--;
document.getElementById('quick-time').textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(gameTimer);
alert(`Temps écoulé ! Score final: ${score}`);
this.onGameEnd(score);
}
}, 1000);
this.showQuickWord();
});
const showQuickWord = () => {
if (currentWordIndex >= words.length) {
clearInterval(gameTimer);
alert(`Bravo ! Score final: ${score}`);
this.onGameEnd(score);
return;
}
const currentWord = words[currentWordIndex];
document.getElementById('current-word').textContent = currentWord.english;
// Créer 4 options (1 correcte + 3 fausses)
const options = [currentWord.french];
const otherWords = words.filter(w => w.french !== currentWord.french);
for (let i = 0; i < 3; i++) {
if (otherWords[i]) {
options.push(otherWords[i].french);
}
}
const shuffledOptions = this.shuffleArray(options);
const optionsContainer = document.getElementById('translation-options');
optionsContainer.innerHTML = '';
shuffledOptions.forEach(option => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.textContent = option;
btn.addEventListener('click', () => {
if (option === currentWord.french) {
score += 5;
btn.classList.add('correct');
setTimeout(() => {
currentWordIndex++;
this.showQuickWord();
}, 500);
} else {
score = Math.max(0, score - 1);
btn.classList.add('wrong');
document.querySelector(`button:contains("${currentWord.french}")`);
}
document.getElementById('quick-score').textContent = score;
this.onScoreUpdate(score);
// Désactiver tous les boutons
document.querySelectorAll('.option-btn').forEach(b => b.disabled = true);
});
optionsContainer.appendChild(btn);
});
};
this.showQuickWord = showQuickWord;
}
// === WORD BUILDER GAME ===
startWordBuilder() {
this.container.innerHTML = `
<div class="mini-game word-builder-game">
<div class="mini-game-header">
<button class="back-to-selector"> Retour</button>
<h3>🔤 Word Builder</h3>
<div class="mini-score">Score: <span id="builder-score">0</span></div>
</div>
<div class="word-builder-board">
<div class="french-word">
<p>Traduction : <strong id="french-hint">---</strong></p>
</div>
<div class="word-construction">
<div class="word-spaces" id="word-spaces">
<!-- Espaces pour les lettres -->
</div>
<div class="available-letters" id="available-letters">
<!-- Lettres disponibles -->
</div>
</div>
<div class="builder-controls">
<button id="next-word-btn" style="display:none;">Mot suivant</button>
<button id="give-up-btn">Passer</button>
</div>
</div>
</div>
`;
this.setupWordBuilder();
}
setupWordBuilder() {
document.querySelector('.back-to-selector').addEventListener('click', () => {
this.showGameSelector();
});
let currentWordIndex = 0;
let score = 0;
const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 8);
const showBuilderWord = () => {
if (currentWordIndex >= words.length) {
alert(`Félicitations ! Score final: ${score}`);
this.onGameEnd(score);
return;
}
const currentWord = words[currentWordIndex];
const wordSpaces = document.getElementById('word-spaces');
const availableLetters = document.getElementById('available-letters');
document.getElementById('french-hint').textContent = currentWord.french;
// Créer les espaces
wordSpaces.innerHTML = '';
currentWord.english.split('').forEach((letter, index) => {
const space = document.createElement('div');
space.className = 'letter-space';
space.dataset.index = index;
space.dataset.letter = letter.toLowerCase();
wordSpaces.appendChild(space);
});
// Créer les lettres mélangées + quelques lettres supplémentaires
const wordLetters = currentWord.english.toLowerCase().split('');
const extraLetters = 'abcdefghijklmnopqrstuvwxyz'.split('')
.filter(l => !wordLetters.includes(l))
.slice(0, 3);
const allLetters = this.shuffleArray([...wordLetters, ...extraLetters]);
availableLetters.innerHTML = '';
allLetters.forEach(letter => {
const letterBtn = document.createElement('button');
letterBtn.className = 'letter-btn';
letterBtn.textContent = letter.toUpperCase();
letterBtn.dataset.letter = letter;
letterBtn.addEventListener('click', () => {
this.placeLetter(letter, letterBtn);
});
availableLetters.appendChild(letterBtn);
});
document.getElementById('next-word-btn').style.display = 'none';
document.getElementById('give-up-btn').style.display = 'inline-block';
};
const placeLetter = (letter, btn) => {
const emptySpace = document.querySelector(
`.letter-space[data-letter="${letter}"]:not(.filled)`
);
if (emptySpace) {
emptySpace.textContent = letter.toUpperCase();
emptySpace.classList.add('filled');
btn.disabled = true;
// Vérifier si le mot est complet
const allSpaces = document.querySelectorAll('.letter-space');
const filledSpaces = document.querySelectorAll('.letter-space.filled');
if (allSpaces.length === filledSpaces.length) {
score += 15;
document.getElementById('builder-score').textContent = score;
this.onScoreUpdate(score);
document.getElementById('next-word-btn').style.display = 'inline-block';
document.getElementById('give-up-btn').style.display = 'none';
// Désactiver toutes les lettres
document.querySelectorAll('.letter-btn').forEach(b => b.disabled = true);
}
} else {
// Mauvaise lettre
btn.classList.add('wrong-letter');
setTimeout(() => btn.classList.remove('wrong-letter'), 1000);
}
};
document.getElementById('next-word-btn').addEventListener('click', () => {
currentWordIndex++;
showBuilderWord();
});
document.getElementById('give-up-btn').addEventListener('click', () => {
currentWordIndex++;
showBuilderWord();
});
this.placeLetter = placeLetter;
showBuilderWord();
}
shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
start() {
// Interface commune - montrer le sélecteur
this.showGameSelector();
}
destroy() {
this.container.innerHTML = '';
}
}
// CSS supplémentaire pour les mini-jeux
const tempGamesStyles = `
<style>
.temp-games-wrapper {
text-align: center;
}
.mini-games-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.mini-game-card {
background: white;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.mini-game-card:hover {
border-color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.mini-game-icon {
font-size: 2.5rem;
margin-bottom: 10px;
}
.mini-game-title {
color: var(--primary-color);
margin-bottom: 8px;
}
.mini-game-description {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 10px;
}
.mini-game-difficulty {
font-weight: bold;
font-size: 0.8rem;
margin-bottom: 15px;
}
.play-mini-game-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
cursor: pointer;
}
.mini-game {
max-width: 800px;
margin: 0 auto;
}
.mini-game-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.word-match-board {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 20px;
}
.word-btn {
display: block;
width: 100%;
padding: 12px;
margin-bottom: 10px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
}
.word-btn:hover {
border-color: var(--primary-color);
}
.word-btn.selected {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.word-btn.matched {
background: var(--secondary-color);
color: white;
border-color: var(--secondary-color);
cursor: default;
}
.translation-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-top: 20px;
}
.option-btn {
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.option-btn:hover {
border-color: var(--primary-color);
}
.option-btn.correct {
background: var(--secondary-color);
color: white;
border-color: var(--secondary-color);
}
.option-btn.wrong {
background: var(--error-color);
color: white;
border-color: var(--error-color);
}
.word-construction {
margin: 30px 0;
}
.word-spaces {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 30px;
}
.letter-space {
width: 40px;
height: 40px;
border: 2px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
background: white;
}
.letter-space.filled {
background: var(--secondary-color);
color: white;
border-color: var(--secondary-color);
}
.available-letters {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.letter-btn {
width: 35px;
height: 35px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: all 0.3s ease;
}
.letter-btn:hover {
border-color: var(--primary-color);
}
.letter-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.letter-btn.wrong-letter {
background: var(--error-color);
color: white;
border-color: var(--error-color);
}
@media (max-width: 768px) {
.word-match-board {
grid-template-columns: 1fr;
gap: 20px;
}
.translation-options {
grid-template-columns: 1fr;
}
.mini-game-header {
flex-direction: column;
gap: 10px;
}
}
</style>
`;
// Ajouter les styles
document.head.insertAdjacentHTML('beforeend', tempGamesStyles);
// Enregistrement du module
window.GameModules = window.GameModules || {};
window.GameModules.TempGames = TempGamesModule;

View File

@ -24,7 +24,7 @@ class TextReaderGame {
init() {
// Vérifier que nous avons des textes
if (!this.texts || this.texts.length === 0) {
console.error('Aucun texte disponible pour Text Reader');
logSh('Aucun texte disponible pour Text Reader', 'ERROR');
this.showInitError();
return;
}
@ -48,24 +48,24 @@ class TextReaderGame {
extractTexts(content) {
let texts = [];
console.log('📖 Extracting texts from:', content?.name || 'content');
logSh('📖 Extracting texts from:', content?.name || 'content', 'INFO');
// Use raw module content if available
if (content.rawContent) {
console.log('📦 Using raw module content');
logSh('📦 Using raw module content', 'INFO');
return this.extractTextsFromRaw(content.rawContent);
}
// Format with texts array
if (content.texts && Array.isArray(content.texts)) {
console.log('📝 Texts format detected');
logSh('📝 Texts format detected', 'INFO');
texts = content.texts.filter(text =>
text.content && text.content.trim() !== ''
);
}
// Modern format with contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 ContentItems format detected');
logSh('🆕 ContentItems format detected', 'INFO');
texts = content.contentItems
.filter(item => item.type === 'text' && item.content)
.map(item => ({
@ -78,7 +78,7 @@ class TextReaderGame {
}
extractTextsFromRaw(rawContent) {
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let texts = [];
// Simple format (texts array)
@ -86,7 +86,7 @@ class TextReaderGame {
texts = rawContent.texts.filter(text =>
text.content && text.content.trim() !== ''
);
console.log(`📝 ${texts.length} texts extracted from texts array`);
logSh(`📝 ${texts.length} texts extracted from texts array`, 'INFO');
}
// ContentItems format
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
@ -96,7 +96,7 @@ class TextReaderGame {
title: item.title || 'Text',
content: item.content
}));
console.log(`🆕 ${texts.length} texts extracted from contentItems`);
logSh(`🆕 ${texts.length} texts extracted from contentItems`, 'INFO');
}
return this.finalizeTexts(texts);
@ -111,7 +111,7 @@ class TextReaderGame {
);
if (texts.length === 0) {
console.error('❌ No valid texts found');
logSh('❌ No valid texts found', 'ERROR');
// Demo texts as fallback
texts = [
{
@ -119,10 +119,10 @@ class TextReaderGame {
content: "This is a demo text. It has multiple sentences. Each sentence will be displayed one by one. You can navigate using the buttons below."
}
];
console.warn('🚨 Using demo texts');
logSh('🚨 Using demo texts', 'WARN');
}
console.log(`✅ Text Reader: ${texts.length} texts finalized`);
logSh(`✅ Text Reader: ${texts.length} texts finalized`, 'INFO');
return texts;
}
@ -196,12 +196,12 @@ class TextReaderGame {
}
start() {
console.log('📖 Text Reader: Starting');
logSh('📖 Text Reader: Starting', 'INFO');
this.isRunning = true;
}
restart() {
console.log('🔄 Text Reader: Restarting');
logSh('🔄 Text Reader: Restarting', 'INFO');
this.reset();
this.start();
}

View File

@ -42,7 +42,7 @@ class WhackAMoleHardGame {
init() {
// Vérifier que nous avons du vocabulaire
if (!this.vocabulary || this.vocabulary.length === 0) {
console.error('Aucun vocabulaire disponible pour Whack-a-Mole');
logSh('Aucun vocabulaire disponible pour Whack-a-Mole', 'ERROR');
this.showInitError();
return;
}
@ -200,7 +200,7 @@ class WhackAMoleHardGame {
// Show loaded content info
const contentName = this.content.name || 'Content';
console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`);
logSh(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`);
}
pause() {
@ -280,7 +280,7 @@ class WhackAMoleHardGame {
}
});
console.log('🔄 Game completely reset');
logSh('🔄 Game completely reset', 'INFO');
}
startTimers() {
@ -356,14 +356,14 @@ class WhackAMoleHardGame {
// Si on a atteint la limite, forcer le mot cible
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`);
logSh(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`, 'INFO');
this.spawnsSinceTarget = 0;
return this.targetWord;
}
// Sinon, 10% de chance d'avoir le mot cible (1/10 au lieu de 1/2)
if (Math.random() < 0.1) {
console.log('🎯 Spawn naturel du mot cible (1/10)');
logSh('🎯 Spawn naturel du mot cible (1/10, 'INFO');');
this.spawnsSinceTarget = 0;
return this.targetWord;
} else {
@ -469,7 +469,7 @@ class WhackAMoleHardGame {
// Reset du compteur pour le nouveau mot cible
this.spawnsSinceTarget = 0;
console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`);
logSh(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`, 'INFO');
document.getElementById('target-word').textContent = this.targetWord.french;
}
@ -519,17 +519,17 @@ class WhackAMoleHardGame {
extractVocabulary(content) {
let vocabulary = [];
console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu');
logSh('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu', 'INFO');
// Priorité 1: Utiliser le contenu brut du module (format simple)
if (content.rawContent) {
console.log('📦 Utilisation du contenu brut du module');
logSh('📦 Utilisation du contenu brut du module', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priorité 2: Format simple avec vocabulary object (nouveau format préféré)
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
console.log('✨ Format simple détecté (vocabulary object)');
logSh('✨ Format simple détecté (vocabulary object, 'INFO');');
vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({
english: english,
french: translation.split('')[0], // Prendre la première traduction si plusieurs
@ -539,12 +539,12 @@ class WhackAMoleHardGame {
}
// Priorité 3: Format legacy avec vocabulary array
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
console.log('📚 Format legacy détecté (vocabulary array)');
logSh('📚 Format legacy détecté (vocabulary array, 'INFO');');
vocabulary = content.vocabulary.filter(word => word.english && word.french);
}
// Priorité 4: Format moderne avec contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 Format contentItems détecté');
logSh('🆕 Format contentItems détecté', 'INFO');
vocabulary = content.contentItems
.filter(item => item.type === 'vocabulary' && item.english && item.french)
.map(item => ({
@ -559,7 +559,7 @@ class WhackAMoleHardGame {
}
extractVocabularyFromRaw(rawContent) {
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
logSh('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Format simple avec vocabulary object (PRÉFÉRÉ)
@ -570,12 +570,12 @@ class WhackAMoleHardGame {
chinese: translation, // Traduction complète en chinois
category: 'general'
}));
console.log(`${vocabulary.length} mots extraits depuis vocabulary object (format simple)`);
logSh(`${vocabulary.length} mots extraits depuis vocabulary object (format simple, 'INFO');`);
}
// Format legacy (vocabulary array)
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`);
logSh(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`, 'INFO');
}
// Format contentItems (ancien format complexe)
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
@ -587,11 +587,11 @@ class WhackAMoleHardGame {
image: item.image || null,
category: item.category || 'general'
}));
console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`);
logSh(`📝 ${vocabulary.length} mots extraits depuis contentItems`, 'INFO');
}
// Fallback
else {
console.warn('⚠️ Format de contenu brut non reconnu');
logSh('⚠️ Format de contenu brut non reconnu', 'WARN');
}
return this.finalizeVocabulary(vocabulary);
@ -608,7 +608,7 @@ class WhackAMoleHardGame {
);
if (vocabulary.length === 0) {
console.error('❌ Aucun vocabulaire valide trouvé');
logSh('❌ Aucun vocabulaire valide trouvé', 'ERROR');
// Vocabulaire de démonstration en dernier recours
vocabulary = [
{ english: 'hello', french: 'bonjour', category: 'greetings' },
@ -617,10 +617,10 @@ class WhackAMoleHardGame {
{ english: 'cat', french: 'chat', category: 'animals' },
{ english: 'dog', french: 'chien', category: 'animals' }
];
console.warn('🚨 Utilisation du vocabulaire de démonstration');
logSh('🚨 Utilisation du vocabulaire de démonstration', 'WARN');
}
console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`);
logSh(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`, 'INFO');
return this.shuffleArray(vocabulary);
}

View File

@ -41,7 +41,7 @@ class WhackAMoleGame {
init() {
// Vérifier que nous avons du vocabulaire
if (!this.vocabulary || this.vocabulary.length === 0) {
console.error('Aucun vocabulaire disponible pour Whack-a-Mole');
logSh('Aucun vocabulaire disponible pour Whack-a-Mole', 'ERROR');
this.showInitError();
return;
}
@ -199,7 +199,7 @@ class WhackAMoleGame {
// Show loaded content info
const contentName = this.content.name || 'Content';
console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`);
logSh(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`);
}
pause() {
@ -279,7 +279,7 @@ class WhackAMoleGame {
}
});
console.log('🔄 Game completely reset');
logSh('🔄 Game completely reset', 'INFO');
}
startTimers() {
@ -336,14 +336,14 @@ class WhackAMoleGame {
// Si on a atteint la limite, forcer le mot cible
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`);
logSh(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`, 'INFO');
this.spawnsSinceTarget = 0;
return this.targetWord;
}
// Sinon, 50% de chance d'avoir le mot cible, 50% un mot aléatoire
if (Math.random() < 0.5) {
console.log('🎯 Spawn naturel du mot cible');
logSh('🎯 Spawn naturel du mot cible', 'INFO');
this.spawnsSinceTarget = 0;
return this.targetWord;
} else {
@ -449,7 +449,7 @@ class WhackAMoleGame {
// Reset du compteur pour le nouveau mot cible
this.spawnsSinceTarget = 0;
console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`);
logSh(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`, 'INFO');
document.getElementById('target-word').textContent = this.targetWord.french;
}
@ -499,17 +499,17 @@ class WhackAMoleGame {
extractVocabulary(content) {
let vocabulary = [];
console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu');
logSh('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu', 'INFO');
// Priorité 1: Utiliser le contenu brut du module (format simple)
if (content.rawContent) {
console.log('📦 Utilisation du contenu brut du module');
logSh('📦 Utilisation du contenu brut du module', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priorité 2: Format simple avec vocabulary object (nouveau format préféré)
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
console.log('✨ Format simple détecté (vocabulary object)');
logSh('✨ Format simple détecté (vocabulary object, 'INFO');');
vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({
english: english,
french: translation.split('')[0], // Prendre la première traduction si plusieurs
@ -519,12 +519,12 @@ class WhackAMoleGame {
}
// Priorité 3: Format legacy avec vocabulary array
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
console.log('📚 Format legacy détecté (vocabulary array)');
logSh('📚 Format legacy détecté (vocabulary array, 'INFO');');
vocabulary = content.vocabulary.filter(word => word.english && word.french);
}
// Priorité 4: Format moderne avec contentItems
else if (content.contentItems && Array.isArray(content.contentItems)) {
console.log('🆕 Format contentItems détecté');
logSh('🆕 Format contentItems détecté', 'INFO');
vocabulary = content.contentItems
.filter(item => item.type === 'vocabulary' && item.english && item.french)
.map(item => ({
@ -539,7 +539,7 @@ class WhackAMoleGame {
}
extractVocabularyFromRaw(rawContent) {
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
logSh('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Format simple avec vocabulary object (PRÉFÉRÉ)
@ -550,12 +550,12 @@ class WhackAMoleGame {
chinese: translation, // Traduction complète en chinois
category: 'general'
}));
console.log(`${vocabulary.length} mots extraits depuis vocabulary object (format simple)`);
logSh(`${vocabulary.length} mots extraits depuis vocabulary object (format simple, 'INFO');`);
}
// Format legacy (vocabulary array)
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`);
logSh(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`, 'INFO');
}
// Format contentItems (ancien format complexe)
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
@ -567,11 +567,11 @@ class WhackAMoleGame {
image: item.image || null,
category: item.category || 'general'
}));
console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`);
logSh(`📝 ${vocabulary.length} mots extraits depuis contentItems`, 'INFO');
}
// Fallback
else {
console.warn('⚠️ Format de contenu brut non reconnu');
logSh('⚠️ Format de contenu brut non reconnu', 'WARN');
}
return this.finalizeVocabulary(vocabulary);
@ -588,7 +588,7 @@ class WhackAMoleGame {
);
if (vocabulary.length === 0) {
console.error('❌ Aucun vocabulaire valide trouvé');
logSh('❌ Aucun vocabulaire valide trouvé', 'ERROR');
// Vocabulaire de démonstration en dernier recours
vocabulary = [
{ english: 'hello', french: 'bonjour', category: 'greetings' },
@ -597,10 +597,10 @@ class WhackAMoleGame {
{ english: 'cat', french: 'chat', category: 'animals' },
{ english: 'dog', french: 'chien', category: 'animals' }
];
console.warn('🚨 Utilisation du vocabulaire de démonstration');
logSh('🚨 Utilisation du vocabulaire de démonstration', 'WARN');
}
console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`);
logSh(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`, 'INFO');
return this.shuffleArray(vocabulary);
}

View File

@ -296,7 +296,7 @@ Bob: Hi! I'm Bob. Nice to meet you!"></textarea>
const input = this.collectInput();
const options = this.collectOptions();
console.log('🏭 Génération de contenu...', { input, options });
logSh('🏭 Génération de contenu...', { input, options }, 'INFO');
const content = await this.factory.createContent(input, options);
this.currentContent = content;
@ -307,7 +307,7 @@ Bob: Hi! I'm Bob. Nice to meet you!"></textarea>
document.getElementById('test-game-btn').style.display = 'inline-block';
} catch (error) {
console.error('Erreur génération:', error);
logSh('Erreur génération:', error, 'ERROR');
this.showError('Erreur lors de la génération: ' + error.message);
}
}

114
migrate-console.js Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env node
// Script de migration des console.log vers logSh
const fs = require('fs');
const path = require('path');
// Fonction pour parcourir récursivement les fichiers
function walkSync(dir, filelist = []) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filepath = path.join(dir, file);
const stat = fs.statSync(filepath);
if (stat.isDirectory()) {
// Skip certains dossiers
if (!['node_modules', '.git', 'export_logger'].includes(file)) {
filelist = walkSync(filepath, filelist);
}
} else if (file.endsWith('.js') && !file.startsWith('migrate-')) {
filelist.push(filepath);
}
});
return filelist;
}
// Fonction pour migrer un fichier
function migrateFile(filepath) {
console.log(`🔄 Migration: ${filepath}`);
let content = fs.readFileSync(filepath, 'utf8');
let modified = false;
// Remplacements avec mapping vers logSh
const replacements = [
// console.log -> logSh avec niveau INFO
{
pattern: /console\.log\((.*?)\);?/g,
replacement: (match, args) => {
// Si c'est déjà un template string avec des variables, le garder tel quel
if (args.includes('`') || args.includes('${')) {
return `logSh(${args}, 'INFO');`;
}
// Sinon, traiter normalement
return `logSh(${args}, 'INFO');`;
}
},
// console.warn -> logSh avec niveau WARN
{
pattern: /console\.warn\((.*?)\);?/g,
replacement: (match, args) => {
return `logSh(${args}, 'WARN');`;
}
},
// console.error -> logSh avec niveau ERROR
{
pattern: /console\.error\((.*?)\);?/g,
replacement: (match, args) => {
return `logSh(${args}, 'ERROR');`;
}
}
];
// Appliquer les remplacements
replacements.forEach(({ pattern, replacement }) => {
const originalContent = content;
content = content.replace(pattern, replacement);
if (content !== originalContent) {
modified = true;
}
});
// Sauvegarder si modifié
if (modified) {
fs.writeFileSync(filepath, content, 'utf8');
console.log(`✅ Migré: ${filepath}`);
return true;
} else {
console.log(`⏸️ Pas de changement: ${filepath}`);
return false;
}
}
// Script principal
function main() {
console.log('🚀 Démarrage migration console vers logSh...');
const projectRoot = __dirname;
const jsFiles = walkSync(projectRoot);
console.log(`📁 Trouvé ${jsFiles.length} fichiers JavaScript`);
let migratedCount = 0;
jsFiles.forEach(filepath => {
if (migrateFile(filepath)) {
migratedCount++;
}
});
console.log(`\n🎉 Migration terminée!`);
console.log(`📊 ${migratedCount} fichiers modifiés sur ${jsFiles.length}`);
console.log(`\n🔍 Pour vérifier, lance:`);
console.log(`grep -r "console\\." --include="*.js" . || echo "✅ Plus de console dans le code!"`);
}
// Lancer le script
if (require.main === module) {
main();
}
module.exports = { migrateFile, walkSync };

44
test-connection.html Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>Test WebSocket Connection</title>
</head>
<body>
<h1>Test WebSocket Connection</h1>
<div id="status">Connecting...</div>
<div id="messages"></div>
<script>
const statusDiv = document.getElementById('status');
const messagesDiv = document.getElementById('messages');
console.log('🔌 Tentative de connexion WebSocket...');
const ws = new WebSocket('ws://localhost:8082');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusDiv.textContent = 'Connecté !';
statusDiv.style.color = 'green';
};
ws.onmessage = (event) => {
console.log('📨 Message reçu:', event.data);
const messageDiv = document.createElement('div');
messageDiv.textContent = `Reçu: ${event.data}`;
messagesDiv.appendChild(messageDiv);
};
ws.onclose = () => {
console.log('❌ Connexion fermée');
statusDiv.textContent = 'Déconnecté';
statusDiv.style.color = 'red';
};
ws.onerror = (error) => {
console.log('❌ Erreur WebSocket:', error);
statusDiv.textContent = 'Erreur de connexion';
statusDiv.style.color = 'red';
};
</script>
</body>
</html>

78
test-logs-interface.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Logs Interface Direct</title>
<style>
body { font-family: monospace; background: #1e1e1e; color: white; padding: 20px; }
.log { margin: 2px 0; padding: 4px; border-left: 3px solid #007bff; }
.ERROR { border-color: #dc3545; }
.WARN { border-color: #ffc107; }
.DEBUG { border-color: #6c757d; }
.INFO { border-color: #17a2b8; }
#status {
padding: 10px;
background: #333;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h2>Test Direct Interface de Logs</h2>
<div id="status">En attente...</div>
<div id="logs"></div>
<script>
const statusDiv = document.getElementById('status');
const logsDiv = document.getElementById('logs');
let messageCount = 0;
console.log('🔌 Tentative connexion WebSocket ws://localhost:8082');
const ws = new WebSocket('ws://localhost:8082');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusDiv.innerHTML = '<span style="color: green;">✅ WebSocket connecté - En attente des logs...</span>';
};
ws.onmessage = (event) => {
messageCount++;
console.log(`📨 Message #${messageCount} reçu:`, event.data);
try {
const logData = JSON.parse(event.data);
const logDiv = document.createElement('div');
logDiv.className = `log ${logData.level}`;
const time = new Date(logData.timestamp).toLocaleTimeString();
logDiv.innerHTML = `<strong>[${time}] ${logData.level}:</strong> ${logData.message}`;
logsDiv.appendChild(logDiv);
// Auto-scroll
logDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
// Update status
statusDiv.innerHTML = `<span style="color: green;">✅ Connecté - ${messageCount} messages reçus</span>`;
} catch (error) {
console.log('❌ Erreur parsing:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'log ERROR';
errorDiv.textContent = `Erreur parsing: ${event.data}`;
logsDiv.appendChild(errorDiv);
}
};
ws.onclose = () => {
console.log('❌ Connexion fermée');
statusDiv.innerHTML = `<span style="color: red;">❌ Déconnecté (${messageCount} messages reçus)</span>`;
};
ws.onerror = (error) => {
console.log('❌ Erreur WebSocket:', error);
statusDiv.innerHTML = '<span style="color: red;">❌ Erreur de connexion</span>';
};
</script>
</body>
</html>

View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<title>Test WebSocket Simple</title>
<style>
body { font-family: monospace; background: #1e1e1e; color: white; }
.log { margin: 2px 0; padding: 4px; border-left: 3px solid #007bff; }
.ERROR { border-color: #dc3545; }
.WARN { border-color: #ffc107; }
.DEBUG { border-color: #6c757d; }
.INFO { border-color: #17a2b8; }
</style>
</head>
<body>
<h2>Test WebSocket Simple</h2>
<div id="status">En attente...</div>
<div id="logs"></div>
<script>
const statusDiv = document.getElementById('status');
const logsDiv = document.getElementById('logs');
let messageCount = 0;
console.log('🔌 Tentative connexion WebSocket ws://localhost:8082');
const ws = new WebSocket('ws://localhost:8082');
ws.onopen = () => {
console.log('✅ WebSocket connecté !');
statusDiv.textContent = 'Connecté !';
statusDiv.style.color = 'green';
// Clear initial content
logsDiv.innerHTML = '<div style="color: green;">✅ WebSocket connecté - En attente des logs...</div>';
};
ws.onmessage = (event) => {
messageCount++;
console.log(`📨 Message #${messageCount} reçu:`, event.data);
try {
const logData = JSON.parse(event.data);
const logDiv = document.createElement('div');
logDiv.className = `log ${logData.level}`;
const time = new Date(logData.timestamp).toLocaleTimeString();
logDiv.innerHTML = `<strong>[${time}] ${logData.level}:</strong> ${logData.message}`;
logsDiv.appendChild(logDiv);
// Auto-scroll
logDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
} catch (error) {
console.log('❌ Erreur parsing:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'log ERROR';
errorDiv.textContent = `Erreur parsing: ${event.data}`;
logsDiv.appendChild(errorDiv);
}
};
ws.onclose = () => {
console.log('❌ Connexion fermée');
statusDiv.textContent = `Déconnecté (${messageCount} messages reçus)`;
statusDiv.style.color = 'red';
};
ws.onerror = (error) => {
console.log('❌ Erreur WebSocket:', error);
statusDiv.textContent = 'Erreur de connexion';
statusDiv.style.color = 'red';
};
</script>
</body>
</html>