mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 07:38:17 +02:00
Merge pull request #105 from R0m1k3/claude/fix-time-offset-bug-01FzrcjKh9o5emsU6hRMMLRL
Add: Système de logging complet pour débuguer le décalage horaire
This commit is contained in:
3 files changed
+329
-6
No files matched your search
@@ -0,0 +1,211 @@
|
|||||||
|
# 🔍 SYSTÈME DE LOGGING POUR DÉBUGUER LE TIMEZONE
|
||||||
|
|
||||||
|
Ce document explique comment utiliser le système de logging complet pour identifier le problème de décalage horaire.
|
||||||
|
|
||||||
|
## 📋 Logs ajoutés
|
||||||
|
|
||||||
|
### 1. **Parser PostgreSQL** (`config/database.js`)
|
||||||
|
Logs chaque conversion TIMESTAMPTZ → ISO string
|
||||||
|
|
||||||
|
### 2. **Synchronisation Google Calendar** (`routes/calendar.routes.js`)
|
||||||
|
Logs ce que Google renvoie et ce qui est stocké
|
||||||
|
|
||||||
|
### 3. **Récupération des événements** (`routes/calendar.routes.js`)
|
||||||
|
Logs ce qui est lu de la DB et envoyé au frontend
|
||||||
|
|
||||||
|
### 4. **Endpoint de diagnostic** (`GET /api/calendar/debug`)
|
||||||
|
Affiche un rapport complet du traitement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 PROCÉDURE COMPLÈTE
|
||||||
|
|
||||||
|
### **ÉTAPE 1 : Rebuild avec les logs**
|
||||||
|
|
||||||
|
```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 : Activer les logs debug**
|
||||||
|
|
||||||
|
Suivre les logs en temps réel :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f notes-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ÉTAPE 4 : Synchroniser avec Google Calendar**
|
||||||
|
|
||||||
|
1. Ouvrez http://localhost:2222
|
||||||
|
2. Allez dans Admin → Google Calendar
|
||||||
|
3. Cliquez sur "🔄 Synchroniser"
|
||||||
|
|
||||||
|
**Observez les logs** dans votre terminal :
|
||||||
|
|
||||||
|
```
|
||||||
|
[SYNC CALENDAR] Événement: "Dr Julian Wlodarczak"
|
||||||
|
[SYNC CALENDAR] - Google startTime brut: {"dateTime":"2024-11-17T10:20:00+01:00"}
|
||||||
|
[SYNC CALENDAR] - startTime extrait: "2024-11-17T10:20:00+01:00"
|
||||||
|
[SYNC CALENDAR] - Type: string
|
||||||
|
[SYNC CALENDAR] - new Date(startTime): 2024-11-17T09:20:00.000Z
|
||||||
|
[SYNC CALENDAR] - Affichage Paris: 17/11/2024 10:20:00
|
||||||
|
```
|
||||||
|
|
||||||
|
**Puis le parser PostgreSQL** :
|
||||||
|
|
||||||
|
```
|
||||||
|
[PARSER TIMESTAMPTZ] Input avec TZ: "2024-11-17 09:20:00+00" → Output: "2024-11-17T09:20:00.000Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
**OU**
|
||||||
|
|
||||||
|
```
|
||||||
|
[PARSER TIMESTAMPTZ] Input sans TZ: "2024-11-17 09:20:00" → ISO+Z: "2024-11-17T09:20:00Z" → Output: "2024-11-17T09:20:00.000Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ÉTAPE 5 : Récupérer les événements**
|
||||||
|
|
||||||
|
Rechargez la page NoteFlow et observez les logs :
|
||||||
|
|
||||||
|
```
|
||||||
|
[GET /events] "Dr Julian Wlodarczak"
|
||||||
|
[GET /events] - start_time de la DB: "2024-11-17T09:20:00.000Z" (type: string)
|
||||||
|
[GET /events] - new Date(): 2024-11-17T09:20:00.000Z
|
||||||
|
[GET /events] - Affichage Paris: 10:20
|
||||||
|
```
|
||||||
|
|
||||||
|
### **ÉTAPE 6 : Utiliser l'endpoint de diagnostic**
|
||||||
|
|
||||||
|
Appelez l'endpoint de diagnostic pour voir un rapport complet :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer VOTRE_TOKEN" \
|
||||||
|
http://localhost:2222/api/calendar/debug | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
**OU** avec le navigateur :
|
||||||
|
|
||||||
|
1. Ouvrez http://localhost:2222
|
||||||
|
2. Ouvrez la console développeur (F12)
|
||||||
|
3. Exécutez :
|
||||||
|
```javascript
|
||||||
|
fetch('/api/calendar/debug', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(console.log)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Résultat attendu** :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2024-11-17T...",
|
||||||
|
"serverTimezone": "UTC",
|
||||||
|
"postgresTimezone": "UTC",
|
||||||
|
"sampleEvent": {
|
||||||
|
"title": "Dr Julian Wlodarczak",
|
||||||
|
"type_col": "timestamp with time zone",
|
||||||
|
"start_time_raw": "2024-11-17T09:20:00.000Z",
|
||||||
|
"start_time_type": "string",
|
||||||
|
"start_text": "2024-11-17 09:20:00+00",
|
||||||
|
"start_utc_formatted": "2024-11-17T09:20:00Z",
|
||||||
|
"start_paris_formatted": "2024-11-17 10:20:00"
|
||||||
|
},
|
||||||
|
"parsing": {
|
||||||
|
"input": "2024-11-17T09:20:00.000Z",
|
||||||
|
"inputType": "string",
|
||||||
|
"dateObject": "2024-11-17T09:20:00.000Z",
|
||||||
|
"displayUTC": "Sun, 17 Nov 2024 09:20:00 GMT",
|
||||||
|
"displayParis": "17/11/2024 10:20:00",
|
||||||
|
"displayParisTime": "10:20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 ANALYSE DES LOGS
|
||||||
|
|
||||||
|
### **Si tout est correct** :
|
||||||
|
|
||||||
|
1. Google renvoie : `"2024-11-17T10:20:00+01:00"` (10:20 Paris)
|
||||||
|
2. Conversion en UTC : `2024-11-17T09:20:00.000Z` (09:20 UTC)
|
||||||
|
3. PostgreSQL stocke : `2024-11-17 09:20:00+00` (09:20 UTC)
|
||||||
|
4. Parser renvoie : `"2024-11-17T09:20:00.000Z"`
|
||||||
|
5. Frontend affiche : `10:20` ✅
|
||||||
|
|
||||||
|
### **Si le décalage persiste** :
|
||||||
|
|
||||||
|
Cherchez dans les logs où la conversion échoue :
|
||||||
|
|
||||||
|
- ❌ **Google renvoie une heure incorrecte** → Problème API Google
|
||||||
|
- ❌ **Conversion UTC incorrecte** → Problème dans `new Date()`
|
||||||
|
- ❌ **PostgreSQL stocke mal** → Problème de timezone PostgreSQL
|
||||||
|
- ❌ **Parser renvoie mal** → Problème dans le parser
|
||||||
|
- ❌ **Frontend affiche mal** → Problème dans `toLocaleTimeString`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 COMMANDES UTILES
|
||||||
|
|
||||||
|
### Voir les logs en temps réel :
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f notes-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voir seulement les logs de timezone :
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f notes-app | grep -E "\[SYNC CALENDAR\]|\[PARSER TIMESTAMPTZ\]|\[GET /events\]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérifier le timezone PostgreSQL :
|
||||||
|
```bash
|
||||||
|
docker exec noteflow-postgres psql -U noteflow -d noteflow -p 5499 -c "SHOW timezone"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voir les événements bruts dans PostgreSQL :
|
||||||
|
```bash
|
||||||
|
docker exec noteflow-postgres psql -U noteflow -d noteflow -p 5499 -c "
|
||||||
|
SELECT
|
||||||
|
title,
|
||||||
|
start_time,
|
||||||
|
start_time::text,
|
||||||
|
start_time AT TIME ZONE 'Europe/Paris' as paris_time
|
||||||
|
FROM calendar_events
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 5
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 RAPPORT À ENVOYER
|
||||||
|
|
||||||
|
Si le problème persiste, envoyez :
|
||||||
|
|
||||||
|
1. **Les logs de synchronisation** (ÉTAPE 4)
|
||||||
|
2. **Les logs de récupération** (ÉTAPE 5)
|
||||||
|
3. **Le résultat de l'endpoint debug** (ÉTAPE 6)
|
||||||
|
4. **Capture d'écran de NoteFlow** montrant le décalage
|
||||||
|
5. **Capture d'écran de Google Calendar** montrant l'heure correcte
|
||||||
|
|
||||||
|
Avec ces informations, je pourrai identifier EXACTEMENT où se produit le décalage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT
|
||||||
|
|
||||||
|
Les logs sont en mode `debug` et peuvent être volumineux. Pour production, pensez à :
|
||||||
|
|
||||||
|
1. Réduire le niveau de log
|
||||||
|
2. Ou désactiver les logs une fois le problème résolu
|
||||||
+15
-6
@@ -11,19 +11,28 @@ types.setTypeParser(1184, function(stringValue) {
|
|||||||
// 1184 = TIMESTAMPTZ
|
// 1184 = TIMESTAMPTZ
|
||||||
if (!stringValue) return null;
|
if (!stringValue) return null;
|
||||||
|
|
||||||
|
// LOG pour debug
|
||||||
|
const originalValue = stringValue;
|
||||||
|
|
||||||
// PostgreSQL avec timezone=UTC renvoie: "2024-11-17 09:20:00" ou "2024-11-17 09:20:00+00"
|
// PostgreSQL avec timezone=UTC renvoie: "2024-11-17 09:20:00" ou "2024-11-17 09:20:00+00"
|
||||||
// On doit toujours renvoyer une ISO string UTC propre avec 'Z'
|
// On doit toujours renvoyer une ISO string UTC propre avec 'Z'
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
// 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})?$/)) {
|
||||||
return new Date(stringValue).toISOString();
|
result = new Date(stringValue).toISOString();
|
||||||
|
logger.debug(`[PARSER TIMESTAMPTZ] Input avec TZ: "${originalValue}" → Output: "${result}"`);
|
||||||
|
} else {
|
||||||
|
// Si format "YYYY-MM-DD HH:MM:SS" sans timezone
|
||||||
|
// Comme timezone=UTC, on sait que c'est en UTC
|
||||||
|
// On ajoute 'Z' pour forcer JavaScript à l'interpréter comme UTC
|
||||||
|
const isoString = stringValue.replace(' ', 'T') + 'Z';
|
||||||
|
result = new Date(isoString).toISOString();
|
||||||
|
logger.debug(`[PARSER TIMESTAMPTZ] Input sans TZ: "${originalValue}" → ISO+Z: "${isoString}" → Output: "${result}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si format "YYYY-MM-DD HH:MM:SS" sans timezone
|
return result;
|
||||||
// Comme timezone=UTC, on sait que c'est en UTC
|
|
||||||
// On ajoute 'Z' pour forcer JavaScript à l'interpréter comme UTC
|
|
||||||
const isoString = stringValue.replace(' ', 'T') + 'Z';
|
|
||||||
return new Date(isoString).toISOString();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configuration PostgreSQL depuis DATABASE_URL ou variables d'environnement
|
// Configuration PostgreSQL depuis DATABASE_URL ou variables d'environnement
|
||||||
|
|||||||
@@ -352,6 +352,17 @@ router.get('/events', authenticateToken, async (req, res) => {
|
|||||||
LIMIT $3
|
LIMIT $3
|
||||||
`, [req.user.id, now, limit]);
|
`, [req.user.id, now, limit]);
|
||||||
|
|
||||||
|
// LOG pour chaque événement récupéré
|
||||||
|
events.forEach(event => {
|
||||||
|
logger.debug(`[GET /events] "${event.title}"`);
|
||||||
|
logger.debug(`[GET /events] - start_time de la DB: "${event.start_time}" (type: ${typeof event.start_time})`);
|
||||||
|
if (event.start_time) {
|
||||||
|
const date = new Date(event.start_time);
|
||||||
|
logger.debug(`[GET /events] - new Date(): ${date.toISOString()}`);
|
||||||
|
logger.debug(`[GET /events] - Affichage Paris: ${date.toLocaleString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
res.json(events || []);
|
res.json(events || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération des événements:', error);
|
logger.error('Erreur lors de la récupération des événements:', error);
|
||||||
@@ -359,6 +370,87 @@ router.get('/events', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/debug
|
||||||
|
* Endpoint de diagnostic détaillé pour le timezone
|
||||||
|
*/
|
||||||
|
router.get('/debug', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const diagnostics = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
postgresTimezone: null,
|
||||||
|
sampleEvent: null,
|
||||||
|
parsing: {
|
||||||
|
input: null,
|
||||||
|
output: null,
|
||||||
|
display: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Récupérer le timezone de PostgreSQL
|
||||||
|
const tzResult = await getOne('SHOW timezone');
|
||||||
|
diagnostics.postgresTimezone = tzResult?.timezone || tzResult?.TimeZone;
|
||||||
|
|
||||||
|
// Récupérer un événement pour test
|
||||||
|
const event = await getOne(`
|
||||||
|
SELECT
|
||||||
|
title,
|
||||||
|
start_time,
|
||||||
|
pg_typeof(start_time) as type_col,
|
||||||
|
start_time::text as start_text,
|
||||||
|
to_char(start_time AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') as start_utc_formatted,
|
||||||
|
to_char(start_time AT TIME ZONE 'Europe/Paris', 'YYYY-MM-DD HH24:MI:SS') as start_paris_formatted
|
||||||
|
FROM calendar_events
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
`, [req.user.id]);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
diagnostics.sampleEvent = {
|
||||||
|
title: event.title,
|
||||||
|
type_col: event.type_col,
|
||||||
|
start_time_raw: event.start_time,
|
||||||
|
start_time_type: typeof event.start_time,
|
||||||
|
start_text: event.start_text,
|
||||||
|
start_utc_formatted: event.start_utc_formatted,
|
||||||
|
start_paris_formatted: event.start_paris_formatted
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test de parsing
|
||||||
|
if (event.start_time) {
|
||||||
|
const date = new Date(event.start_time);
|
||||||
|
diagnostics.parsing = {
|
||||||
|
input: event.start_time,
|
||||||
|
inputType: typeof event.start_time,
|
||||||
|
dateObject: date.toISOString(),
|
||||||
|
displayUTC: date.toUTCString(),
|
||||||
|
displayParis: date.toLocaleString('fr-FR', {
|
||||||
|
timeZone: 'Europe/Paris',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}),
|
||||||
|
displayParisTime: date.toLocaleTimeString('fr-FR', {
|
||||||
|
timeZone: 'Europe/Paris',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(diagnostics);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors du diagnostic:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/calendar/sync
|
* POST /api/calendar/sync
|
||||||
* Synchroniser avec Google Calendar (OAuth2, Service Account ou API Key)
|
* Synchroniser avec Google Calendar (OAuth2, Service Account ou API Key)
|
||||||
@@ -410,6 +502,17 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
const endTime = event.end.dateTime || event.end.date;
|
const endTime = event.end.dateTime || event.end.date;
|
||||||
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
|
||||||
|
logger.info(`[SYNC CALENDAR] Événement: "${event.summary}"`);
|
||||||
|
logger.info(`[SYNC CALENDAR] - Google startTime brut: ${JSON.stringify(event.start)}`);
|
||||||
|
logger.info(`[SYNC CALENDAR] - startTime extrait: "${startTime}"`);
|
||||||
|
logger.info(`[SYNC CALENDAR] - Type: ${typeof startTime}`);
|
||||||
|
if (startTime) {
|
||||||
|
const testDate = new Date(startTime);
|
||||||
|
logger.info(`[SYNC CALENDAR] - new Date(startTime): ${testDate.toISOString()}`);
|
||||||
|
logger.info(`[SYNC CALENDAR] - Affichage Paris: ${testDate.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier si l'événement existe déjà
|
// Vérifier si l'événement existe déjà
|
||||||
const existing = await getOne(
|
const existing = await getOne(
|
||||||
'SELECT id FROM calendar_events WHERE google_event_id = $1 AND user_id = $2',
|
'SELECT id FROM calendar_events WHERE google_event_id = $1 AND user_id = $2',
|
||||||
|
|||||||
Reference in new issue
Block a user