mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Add: Système de logs visibles dans le navigateur (sans accès Docker)
NOUVEAU SYSTÈME DE LOGGING: Au lieu d'utiliser les logs Docker (inaccessibles depuis Portainer), création d'un système de logs accessible directement dans le navigateur. FICHIERS AJOUTÉS: 1. services/timezone-logger.js: - Service singleton qui stocke les logs en mémoire - Limite à 500 logs max (pour éviter les fuites mémoire) - Méthodes: log(), getLogs(), clearLogs(), getLogsByCategory() 2. routes/timezone-logs.routes.js: - GET /api/timezone-logs - Logs en JSON - GET /api/timezone-logs/html - Interface web avec design moderne - GET /api/timezone-logs/category/:cat - Filtrer par catégorie - POST /api/timezone-logs/clear - Vider les logs 3. VOIR-LOGS-TIMEZONE.md: - Documentation complète du système - Procédure pas à pas - Explications des logs attendus FICHIERS MODIFIÉS: 1. config/database.js: - Utilise timezoneLogger au lieu de logger.debug - Catégorie: PARSER 2. routes/calendar.routes.js: - Logs avec timezoneLogger lors de la synchronisation - Catégorie: SYNC (synchronisation Google Calendar) - Catégorie: GET (envoi au frontend) 3. server.js: - Ajout de la route /api/timezone-logs INTERFACE WEB DE LOGS: Accessible sur: http://localhost:2222/api/timezone-logs/html Fonctionnalités: - ✅ Design moderne avec dégradés et animations - ✅ Statistiques en temps réel (total, par catégorie) - ✅ Filtrage par catégorie (SYNC, PARSER, GET) - ✅ Auto-refresh toutes les 3 secondes - ✅ Code couleur par catégorie - ✅ Affichage des données JSON formatées - ✅ Bouton pour vider les logs - ✅ Responsive, fonctionne sur mobile UTILISATION: 1. Rebuild: docker-compose build --no-cache notes-app 2. Ouvrir: http://localhost:2222/api/timezone-logs/html 3. Synchroniser Google Calendar 4. Observer les logs en temps réel dans le navigateur Plus besoin d'accès aux logs Docker/Portainer !
This commit is contained in:
6 files changed
+594
-12
No files matched your search
@@ -0,0 +1,190 @@
|
|||||||
|
# 🔍 VOIR LES LOGS DE TIMEZONE DIRECTEMENT DANS VOTRE NAVIGATEUR
|
||||||
|
|
||||||
|
Ce système vous permet de voir en temps réel ce qui se passe avec les dates, **sans avoir besoin d'accès aux logs Docker**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 COMMENT UTILISER
|
||||||
|
|
||||||
|
### **ÉTAPE 1 : Rebuild et redémarrage**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache notes-app
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ÉTAPE 2 : Supprimer les anciens événements**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec noteflow-postgres psql -U noteflow -d noteflow -p 5499 \
|
||||||
|
-c "DELETE FROM calendar_events"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ÉTAPE 3 : Ouvrir la page de logs**
|
||||||
|
|
||||||
|
Ouvrez votre navigateur et allez sur :
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:2222/api/timezone-logs/html
|
||||||
|
```
|
||||||
|
|
||||||
|
**Laissez cette page ouverte** dans un onglet.
|
||||||
|
|
||||||
|
### **ÉTAPE 4 : Synchroniser avec Google Calendar**
|
||||||
|
|
||||||
|
1. Dans un autre onglet, ouvrez http://localhost:2222
|
||||||
|
2. Allez dans **Admin** → **Google Calendar**
|
||||||
|
3. Cliquez sur **🔄 Synchroniser**
|
||||||
|
|
||||||
|
### **ÉTAPE 5 : Regarder les logs apparaître**
|
||||||
|
|
||||||
|
Retournez sur l'onglet des logs (`/api/timezone-logs/html`) et vous verrez :
|
||||||
|
|
||||||
|
- 🟣 **SYNC** : Ce que Google Calendar renvoie
|
||||||
|
- 🟠 **PARSER** : Comment PostgreSQL parse les dates
|
||||||
|
- 🟢 **GET** : Ce qui est envoyé au frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 FONCTIONNALITÉS DE LA PAGE DE LOGS
|
||||||
|
|
||||||
|
### **Boutons en haut** :
|
||||||
|
|
||||||
|
- **🔄 Rafraîchir** : Recharger la page pour voir les nouveaux logs
|
||||||
|
- **🗑️ Vider les logs** : Supprimer tous les logs
|
||||||
|
- **⏱️ Auto-refresh** : Activer le rafraîchissement automatique toutes les 3 secondes
|
||||||
|
- **Filtres** : Afficher seulement les logs d'une catégorie (Tous, Sync, Parser, Get)
|
||||||
|
|
||||||
|
### **Statistiques affichées** :
|
||||||
|
|
||||||
|
- Total de logs
|
||||||
|
- Nombre de logs par catégorie
|
||||||
|
|
||||||
|
### **Chaque log affiche** :
|
||||||
|
|
||||||
|
- ⏰ Timestamp (date et heure exacte)
|
||||||
|
- 🏷️ Catégorie (SYNC, PARSER, GET)
|
||||||
|
- 📝 Message descriptif
|
||||||
|
- 📦 Données JSON (si disponibles)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 EXEMPLE DE LOGS ATTENDUS
|
||||||
|
|
||||||
|
### **Lors de la synchronisation** :
|
||||||
|
|
||||||
|
```
|
||||||
|
[SYNC] 📅 Événement: "Dr Julian Wlodarczak"
|
||||||
|
{
|
||||||
|
"googleStartBrut": {
|
||||||
|
"dateTime": "2024-11-17T10:20:00+01:00"
|
||||||
|
},
|
||||||
|
"startTimeExtrait": "2024-11-17T10:20:00+01:00",
|
||||||
|
"type": "string",
|
||||||
|
"isAllDay": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[SYNC] → Conversion: new Date("2024-11-17T10:20:00+01:00") = 2024-11-17T09:20:00.000Z
|
||||||
|
{
|
||||||
|
"affichageParis": "17/11/2024 10:20:00",
|
||||||
|
"heureParisSeule": "10:20"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Lors du parsing PostgreSQL** :
|
||||||
|
|
||||||
|
```
|
||||||
|
[PARSER] Input sans TZ: "2024-11-17 09:20:00" → ISO+Z: "2024-11-17T09:20:00Z" → Output: "2024-11-17T09:20:00.000Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Lors de l'envoi au frontend** :
|
||||||
|
|
||||||
|
```
|
||||||
|
[GET] 📤 Envoi au frontend: "Dr Julian Wlodarczak"
|
||||||
|
{
|
||||||
|
"start_time_DB": "2024-11-17T09:20:00.000Z",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
[GET] → Frontend recevra: 2024-11-17T09:20:00.000Z
|
||||||
|
{
|
||||||
|
"apresNewDate": "2024-11-17T09:20:00.000Z",
|
||||||
|
"affichageParisAttendu": "10:20"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ CE QU'IL FAUT VÉRIFIER
|
||||||
|
|
||||||
|
Dans les logs, vérifiez que :
|
||||||
|
|
||||||
|
1. ✅ **Google renvoie** : `"2024-11-17T10:20:00+01:00"` (10:20 heure de Paris)
|
||||||
|
2. ✅ **Conversion UTC** : `2024-11-17T09:20:00.000Z` (09:20 UTC = 10:20 Paris)
|
||||||
|
3. ✅ **PostgreSQL stocke** : `"2024-11-17 09:20:00"` (09:20 UTC)
|
||||||
|
4. ✅ **Parser normalise** : `"2024-11-17T09:20:00.000Z"`
|
||||||
|
5. ✅ **Frontend reçoit** : `"2024-11-17T09:20:00.000Z"`
|
||||||
|
6. ✅ **Affichage attendu** : `"10:20"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 COULEURS DANS L'INTERFACE
|
||||||
|
|
||||||
|
- 🟣 **Violet** : Logs de synchronisation (SYNC)
|
||||||
|
- 🟠 **Orange** : Logs du parser PostgreSQL (PARSER)
|
||||||
|
- 🟢 **Vert** : Logs d'envoi au frontend (GET)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 ACCESSIBLE DEPUIS PORTAINER
|
||||||
|
|
||||||
|
Si vous utilisez Portainer :
|
||||||
|
|
||||||
|
1. Ouvrez Portainer
|
||||||
|
2. Cliquez sur votre container `notes-app`
|
||||||
|
3. Dans le navigateur, allez sur : `http://localhost:2222/api/timezone-logs/html`
|
||||||
|
|
||||||
|
Vous pouvez aussi accéder à la page depuis **n'importe quel appareil** sur le même réseau :
|
||||||
|
|
||||||
|
```
|
||||||
|
http://VOTRE_IP:2222/api/timezone-logs/html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 SI VOUS VOYEZ UN DÉCALAGE
|
||||||
|
|
||||||
|
Prenez une **capture d'écran de la page de logs** et cherchez :
|
||||||
|
|
||||||
|
- ❌ Où l'heure change de 10:20 à 11:20
|
||||||
|
- ❌ Quelle étape cause le décalage (SYNC, PARSER, ou GET)
|
||||||
|
|
||||||
|
Envoyez-moi la capture et je pourrai corriger le problème exact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 AUTRES ENDPOINTS DISPONIBLES
|
||||||
|
|
||||||
|
### **Logs en JSON** :
|
||||||
|
```
|
||||||
|
GET http://localhost:2222/api/timezone-logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Vider les logs** :
|
||||||
|
```
|
||||||
|
POST http://localhost:2222/api/timezone-logs/clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Logs d'une catégorie** :
|
||||||
|
```
|
||||||
|
GET http://localhost:2222/api/timezone-logs/category/SYNC
|
||||||
|
GET http://localhost:2222/api/timezone-logs/category/PARSER
|
||||||
|
GET http://localhost:2222/api/timezone-logs/category/GET
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 ASTUCE
|
||||||
|
|
||||||
|
Activez l'**Auto-refresh** pour voir les logs apparaître en temps réel pendant que vous synchronisez !
|
||||||
+3
-2
@@ -2,6 +2,7 @@
|
|||||||
const { Pool, types } = require('pg');
|
const { Pool, types } = require('pg');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const timezoneLogger = require('../services/timezone-logger');
|
||||||
|
|
||||||
// IMPORTANT: Parser personnalisé pour TIMESTAMPTZ
|
// IMPORTANT: Parser personnalisé pour TIMESTAMPTZ
|
||||||
// On force PostgreSQL à renvoyer en UTC (options: '-c timezone=UTC')
|
// On force PostgreSQL à renvoyer en UTC (options: '-c timezone=UTC')
|
||||||
@@ -22,14 +23,14 @@ types.setTypeParser(1184, function(stringValue) {
|
|||||||
// Si déjà au format ISO avec Z ou timezone (+HH:MM ou +HH)
|
// Si déjà au format ISO avec Z ou timezone (+HH:MM ou +HH)
|
||||||
if (stringValue.includes('Z') || stringValue.match(/[+-]\d{2}(:\d{2})?$/)) {
|
if (stringValue.includes('Z') || stringValue.match(/[+-]\d{2}(:\d{2})?$/)) {
|
||||||
result = new Date(stringValue).toISOString();
|
result = new Date(stringValue).toISOString();
|
||||||
logger.debug(`[PARSER TIMESTAMPTZ] Input avec TZ: "${originalValue}" → Output: "${result}"`);
|
timezoneLogger.log('PARSER', `Input avec TZ: "${originalValue}" → Output: "${result}"`);
|
||||||
} else {
|
} else {
|
||||||
// Si format "YYYY-MM-DD HH:MM:SS" sans timezone
|
// Si format "YYYY-MM-DD HH:MM:SS" sans timezone
|
||||||
// Comme timezone=UTC, on sait que c'est en UTC
|
// Comme timezone=UTC, on sait que c'est en UTC
|
||||||
// On ajoute 'Z' pour forcer JavaScript à l'interpréter comme UTC
|
// On ajoute 'Z' pour forcer JavaScript à l'interpréter comme UTC
|
||||||
const isoString = stringValue.replace(' ', 'T') + 'Z';
|
const isoString = stringValue.replace(' ', 'T') + 'Z';
|
||||||
result = new Date(isoString).toISOString();
|
result = new Date(isoString).toISOString();
|
||||||
logger.debug(`[PARSER TIMESTAMPTZ] Input sans TZ: "${originalValue}" → ISO+Z: "${isoString}" → Output: "${result}"`);
|
timezoneLogger.log('PARSER', `Input sans TZ: "${originalValue}" → ISO+Z: "${isoString}" → Output: "${result}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+21
-10
@@ -5,6 +5,7 @@ const { google } = require('googleapis');
|
|||||||
const { getAll, getOne, runQuery } = require('../config/database');
|
const { getAll, getOne, runQuery } = require('../config/database');
|
||||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../config/logger');
|
const logger = require('../config/logger');
|
||||||
|
const timezoneLogger = require('../services/timezone-logger');
|
||||||
|
|
||||||
// Scopes Google Calendar requis (lecture ET écriture)
|
// Scopes Google Calendar requis (lecture ET écriture)
|
||||||
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
|
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
|
||||||
@@ -354,12 +355,17 @@ router.get('/events', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// LOG pour chaque événement récupéré
|
// LOG pour chaque événement récupéré
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
logger.debug(`[GET /events] "${event.title}"`);
|
timezoneLogger.log('GET', `📤 Envoi au frontend: "${event.title}"`, {
|
||||||
logger.debug(`[GET /events] - start_time de la DB: "${event.start_time}" (type: ${typeof event.start_time})`);
|
start_time_DB: event.start_time,
|
||||||
|
type: typeof event.start_time
|
||||||
|
});
|
||||||
|
|
||||||
if (event.start_time) {
|
if (event.start_time) {
|
||||||
const date = new Date(event.start_time);
|
const date = new Date(event.start_time);
|
||||||
logger.debug(`[GET /events] - new Date(): ${date.toISOString()}`);
|
timezoneLogger.log('GET', ` → Frontend recevra: ${event.start_time}`, {
|
||||||
logger.debug(`[GET /events] - Affichage Paris: ${date.toLocaleString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })}`);
|
apresNewDate: date.toISOString(),
|
||||||
|
affichageParisAttendu: date.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -503,14 +509,19 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
const isAllDay = !event.start.dateTime; // Si pas de dateTime, c'est un événement toute la journée
|
const isAllDay = !event.start.dateTime; // Si pas de dateTime, c'est un événement toute la journée
|
||||||
|
|
||||||
// LOG DÉTAILLÉ pour debug timezone
|
// LOG DÉTAILLÉ pour debug timezone
|
||||||
logger.info(`[SYNC CALENDAR] Événement: "${event.summary}"`);
|
timezoneLogger.log('SYNC', `📅 Événement: "${event.summary}"`, {
|
||||||
logger.info(`[SYNC CALENDAR] - Google startTime brut: ${JSON.stringify(event.start)}`);
|
googleStartBrut: event.start,
|
||||||
logger.info(`[SYNC CALENDAR] - startTime extrait: "${startTime}"`);
|
startTimeExtrait: startTime,
|
||||||
logger.info(`[SYNC CALENDAR] - Type: ${typeof startTime}`);
|
type: typeof startTime,
|
||||||
|
isAllDay
|
||||||
|
});
|
||||||
|
|
||||||
if (startTime) {
|
if (startTime) {
|
||||||
const testDate = new Date(startTime);
|
const testDate = new Date(startTime);
|
||||||
logger.info(`[SYNC CALENDAR] - new Date(startTime): ${testDate.toISOString()}`);
|
timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTime}") = ${testDate.toISOString()}`, {
|
||||||
logger.info(`[SYNC CALENDAR] - Affichage Paris: ${testDate.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })}`);
|
affichageParis: testDate.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }),
|
||||||
|
heureParisSeule: testDate.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si l'événement existe déjà
|
// Vérifier si l'événement existe déjà
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
// Route pour afficher les logs de timezone
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
|
const timezoneLogger = require('../services/timezone-logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/timezone-logs
|
||||||
|
* Afficher tous les logs de timezone
|
||||||
|
*/
|
||||||
|
router.get('/', authenticateToken, (req, res) => {
|
||||||
|
const limit = parseInt(req.query.limit) || 100;
|
||||||
|
const logs = timezoneLogger.getLogs(limit);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total: logs.length,
|
||||||
|
limit,
|
||||||
|
logs
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/timezone-logs/category/:category
|
||||||
|
* Afficher les logs d'une catégorie spécifique
|
||||||
|
*/
|
||||||
|
router.get('/category/:category', authenticateToken, (req, res) => {
|
||||||
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
|
const logs = timezoneLogger.getLogsByCategory(req.category.toUpperCase(), limit);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
category: req.params.category,
|
||||||
|
total: logs.length,
|
||||||
|
limit,
|
||||||
|
logs
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/timezone-logs/clear
|
||||||
|
* Vider tous les logs
|
||||||
|
*/
|
||||||
|
router.post('/clear', authenticateToken, (req, res) => {
|
||||||
|
timezoneLogger.clearLogs();
|
||||||
|
res.json({ message: 'Logs vidés avec succès' });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/timezone-logs/html
|
||||||
|
* Afficher les logs en HTML (pour voir directement dans le navigateur)
|
||||||
|
*/
|
||||||
|
router.get('/html', authenticateToken, (req, res) => {
|
||||||
|
const limit = parseInt(req.query.limit) || 200;
|
||||||
|
const logs = timezoneLogger.getLogs(limit);
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Logs Timezone - NoteFlow</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: white;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header .stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
button.danger {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
button.danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
button.success {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
button.success:hover {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
background: #1e293b;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.log-entry:hover {
|
||||||
|
background: #2d3b52;
|
||||||
|
transform: translateX(4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.log-entry.SYNC {
|
||||||
|
border-left-color: #8b5cf6;
|
||||||
|
}
|
||||||
|
.log-entry.PARSER {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
.log-entry.GET {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
.log-timestamp {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.log-category {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.log-category.SYNC {
|
||||||
|
background: #8b5cf6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.log-category.PARSER {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.log-category.GET {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.log-message {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.log-data {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.log-data pre {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.no-logs {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.filter-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
.filter-btn.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔍 Logs de débogage Timezone</h1>
|
||||||
|
<p style="opacity: 0.9; margin-top: 10px;">Suivi en temps réel des conversions de dates</p>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${logs.length}</div>
|
||||||
|
<div class="stat-label">Total logs</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${logs.filter(l => l.category === 'SYNC').length}</div>
|
||||||
|
<div class="stat-label">Synchronisation</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${logs.filter(l => l.category === 'PARSER').length}</div>
|
||||||
|
<div class="stat-label">Parser</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value">${logs.filter(l => l.category === 'GET').length}</div>
|
||||||
|
<div class="stat-label">Frontend</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="location.reload()" class="success">🔄 Rafraîchir</button>
|
||||||
|
<button onclick="clearLogs()" class="danger">🗑️ Vider les logs</button>
|
||||||
|
<button onclick="autoRefresh()" id="autoRefreshBtn">⏱️ Auto-refresh OFF</button>
|
||||||
|
<div class="filter-buttons" style="margin-left: auto;">
|
||||||
|
<button class="filter-btn active" onclick="filterLogs('ALL')">Tous</button>
|
||||||
|
<button class="filter-btn" onclick="filterLogs('SYNC')">Sync</button>
|
||||||
|
<button class="filter-btn" onclick="filterLogs('PARSER')">Parser</button>
|
||||||
|
<button class="filter-btn" onclick="filterLogs('GET')">Get</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="logs-container">
|
||||||
|
${logs.length === 0 ? '<div class="no-logs">Aucun log pour le moment. Synchronisez avec Google Calendar pour voir les logs.</div>' : ''}
|
||||||
|
${logs.map(log => `
|
||||||
|
<div class="log-entry ${log.category}" data-category="${log.category}">
|
||||||
|
<div>
|
||||||
|
<span class="log-timestamp">${new Date(log.timestamp).toLocaleString('fr-FR')}</span>
|
||||||
|
<span class="log-category ${log.category}">${log.category}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-message">${log.message}</div>
|
||||||
|
${log.data ? `<div class="log-data"><pre>${log.data}</pre></div>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let autoRefreshInterval = null;
|
||||||
|
|
||||||
|
function filterLogs(category) {
|
||||||
|
const entries = document.querySelectorAll('.log-entry');
|
||||||
|
const buttons = document.querySelectorAll('.filter-btn');
|
||||||
|
|
||||||
|
buttons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (category === 'ALL' || entry.dataset.category === category) {
|
||||||
|
entry.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
entry.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
if (confirm('Voulez-vous vraiment vider tous les logs ?')) {
|
||||||
|
fetch('/api/timezone-logs/clear', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
}).then(() => location.reload());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoRefresh() {
|
||||||
|
const btn = document.getElementById('autoRefreshBtn');
|
||||||
|
if (autoRefreshInterval) {
|
||||||
|
clearInterval(autoRefreshInterval);
|
||||||
|
autoRefreshInterval = null;
|
||||||
|
btn.textContent = '⏱️ Auto-refresh OFF';
|
||||||
|
btn.style.background = '#3b82f6';
|
||||||
|
} else {
|
||||||
|
autoRefreshInterval = setInterval(() => location.reload(), 3000);
|
||||||
|
btn.textContent = '⏱️ Auto-refresh ON (3s)';
|
||||||
|
btn.style.background = '#10b981';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -93,6 +93,7 @@ app.use('/api/settings', require('./routes/settings.routes'));
|
|||||||
app.use('/api/rss', require('./routes/rss.routes'));
|
app.use('/api/rss', require('./routes/rss.routes'));
|
||||||
app.use('/api/calendar', require('./routes/calendar.routes'));
|
app.use('/api/calendar', require('./routes/calendar.routes'));
|
||||||
app.use('/api/openrouter', require('./routes/openrouter.routes'));
|
app.use('/api/openrouter', require('./routes/openrouter.routes'));
|
||||||
|
app.use('/api/timezone-logs', require('./routes/timezone-logs.routes'));
|
||||||
|
|
||||||
// Route de recherche
|
// Route de recherche
|
||||||
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Service de logging en mémoire pour débuguer le timezone
|
||||||
|
// Les logs sont stockés en mémoire et accessibles via /api/calendar/timezone-logs
|
||||||
|
|
||||||
|
class TimezoneLogger {
|
||||||
|
constructor() {
|
||||||
|
this.logs = [];
|
||||||
|
this.maxLogs = 500; // Garder max 500 logs
|
||||||
|
}
|
||||||
|
|
||||||
|
log(category, message, data = null) {
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
data: data ? JSON.stringify(data, null, 2) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logs.push(entry);
|
||||||
|
|
||||||
|
// Limiter le nombre de logs
|
||||||
|
if (this.logs.length > this.maxLogs) {
|
||||||
|
this.logs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher aussi dans les logs console
|
||||||
|
console.log(`[${category}] ${message}`, data || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogs(limit = 100) {
|
||||||
|
return this.logs.slice(-limit).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLogs() {
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogsByCategory(category, limit = 50) {
|
||||||
|
return this.logs
|
||||||
|
.filter(log => log.category === category)
|
||||||
|
.slice(-limit)
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
const timezoneLogger = new TimezoneLogger();
|
||||||
|
|
||||||
|
module.exports = timezoneLogger;
|
||||||
Reference in new issue
Block a user