mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Fix: Timezone Europe/Paris + diagnostic complet calendrier
Problème: Décalage d'1 heure persistant malgré types.setTypeParser Cause probable: Container pas rebuild avec le fix database.js Ajouts: 1. scripts/check-calendar-timezone.js - Diagnostic complet timezone PostgreSQL/Node.js - Vérifie si types.setTypeParser(1184) fonctionne - Affiche type de données retourné (string vs Date object) - Compare valeurs SQL brutes avec valeurs JS 2. CALENDAR_TIMEZONE_FIX.md - Documentation complète du problème - Procédure de fix étape par étape - Explication technique de la cause - Historique des tentatives Utilisation: ```bash # 1. Diagnostic node scripts/check-calendar-timezone.js # 2. Si Date object au lieu de string: docker-compose build --no-cache notes-app docker-compose up -d # 3. Resynchroniser calendrier depuis UI ``` Le fix types.setTypeParser(1184) est déjà en place dans database.js mais nécessite un rebuild du container pour être actif.
This commit is contained in:
2 files changed
+268
No files matched your search
@@ -0,0 +1,157 @@
|
|||||||
|
# Fix Décalage Horaire Calendrier
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
|
||||||
|
Les événements du calendrier Google s'affichent avec 1 heure de décalage dans NoteFlow.
|
||||||
|
|
||||||
|
## Cause Racine
|
||||||
|
|
||||||
|
Le driver PostgreSQL (`pg`) convertit automatiquement les colonnes `TIMESTAMPTZ` en objets `Date` JavaScript, causant une double conversion de timezone:
|
||||||
|
1. Google Calendar → PostgreSQL (stockage en UTC)
|
||||||
|
2. PostgreSQL → JavaScript Date (conversion automatique en timezone locale)
|
||||||
|
3. Frontend → Affichage (nouvelle conversion)
|
||||||
|
|
||||||
|
## Solution Appliquée
|
||||||
|
|
||||||
|
### 1. Désactivation du parsing automatique (config/database.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Pool, types } = require('pg');
|
||||||
|
|
||||||
|
// IMPORTANT: Désactiver le parsing automatique des TIMESTAMPTZ
|
||||||
|
types.setTypeParser(1184, function(stringValue) {
|
||||||
|
return stringValue; // Renvoie la string ISO au lieu d'un objet Date
|
||||||
|
});
|
||||||
|
|
||||||
|
const pool = new Pool({...});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Frontend avec timezone explicite (src/pages/Index.tsx)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
startDate.toLocaleTimeString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZone: 'Europe/Paris'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Procédure de Fix
|
||||||
|
|
||||||
|
### Étape 1: Diagnostic
|
||||||
|
|
||||||
|
Vérifier si le fix est actif:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/check-calendar-timezone.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce script va vérifier:
|
||||||
|
- ✓ Si `start_time` est retourné comme `string` (bon)
|
||||||
|
- ✗ Si `start_time` est un `Date` object (mauvais - rebuild requis)
|
||||||
|
|
||||||
|
### Étape 2: Rebuild du Container (SI NÉCESSAIRE)
|
||||||
|
|
||||||
|
Si le diagnostic montre que les dates sont des `Date` objects:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Arrêter le container
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 2. Rebuild SANS cache (important!)
|
||||||
|
docker-compose build --no-cache notes-app
|
||||||
|
|
||||||
|
# 3. Redémarrer
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 4. Vérifier les logs
|
||||||
|
docker-compose logs -f notes-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 3: Resynchronisation
|
||||||
|
|
||||||
|
1. Ouvrir l'interface web NoteFlow
|
||||||
|
2. Aller dans Admin → Calendrier
|
||||||
|
3. Cliquer sur "Synchroniser avec Google Calendar"
|
||||||
|
4. Vérifier que les heures affichées sont correctes
|
||||||
|
|
||||||
|
### Étape 4: Si le problème persiste
|
||||||
|
|
||||||
|
Supprimer les événements et re-synchroniser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se connecter à PostgreSQL
|
||||||
|
docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499
|
||||||
|
|
||||||
|
# Supprimer tous les événements
|
||||||
|
DELETE FROM calendar_events;
|
||||||
|
|
||||||
|
# Quitter
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis re-synchroniser depuis l'interface web.
|
||||||
|
|
||||||
|
## Vérification Finale
|
||||||
|
|
||||||
|
Comparer un événement dans Google Calendar et NoteFlow:
|
||||||
|
|
||||||
|
**Google Calendar:**
|
||||||
|
- Rendez-vous: 14:00
|
||||||
|
|
||||||
|
**NoteFlow (devrait afficher):**
|
||||||
|
- Rendez-vous: 14:00 ✓
|
||||||
|
|
||||||
|
**Si NoteFlow affiche:**
|
||||||
|
- Rendez-vous: 15:00 ✗ → Reprendre la procédure depuis l'étape 2
|
||||||
|
|
||||||
|
## Notes Techniques
|
||||||
|
|
||||||
|
### Types PostgreSQL
|
||||||
|
|
||||||
|
- `TIMESTAMP` - Sans timezone, stocke la valeur brute
|
||||||
|
- `TIMESTAMPTZ` - Avec timezone, stocke en UTC + timezone
|
||||||
|
|
||||||
|
### Type Parser ID
|
||||||
|
|
||||||
|
- `1184` = TIMESTAMPTZ (timestamp with time zone)
|
||||||
|
- `1114` = TIMESTAMP (timestamp without time zone)
|
||||||
|
|
||||||
|
### Timezone PostgreSQL
|
||||||
|
|
||||||
|
Par défaut, PostgreSQL utilise UTC:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SHOW timezone;
|
||||||
|
-- Résultat: UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timezone Node.js/Container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo $TZ
|
||||||
|
# Si vide, Node.js utilise la timezone système (généralement UTC dans Docker)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format des Dates Google Calendar
|
||||||
|
|
||||||
|
Google Calendar API retourne:
|
||||||
|
- `event.start.dateTime`: `"2025-11-15T14:00:00+01:00"` (événement avec heure)
|
||||||
|
- `event.start.date`: `"2025-11-15"` (événement toute la journée)
|
||||||
|
|
||||||
|
Ces valeurs sont stockées telles quelles dans PostgreSQL TIMESTAMPTZ.
|
||||||
|
|
||||||
|
## Historique des Tentatives
|
||||||
|
|
||||||
|
1. ✗ Changed TIMESTAMP → TIMESTAMPTZ
|
||||||
|
2. ✗ Migration with 'AT TIME ZONE UTC'
|
||||||
|
3. ✗ Migration with 'AT TIME ZONE Europe/Paris'
|
||||||
|
4. ✗ Added timeZone: 'Europe/Paris' to frontend
|
||||||
|
5. ✓ **types.setTypeParser(1184)** - Solution finale
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Si le problème persiste après toutes ces étapes, vérifier:
|
||||||
|
1. Version de `pg` dans package.json
|
||||||
|
2. Logs backend pendant la récupération des événements
|
||||||
|
3. Network tab du navigateur pour voir les données brutes retournées par l'API
|
||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de diagnostic pour vérifier le parsing des TIMESTAMPTZ
|
||||||
|
* et le décalage horaire dans le calendrier
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { getAll, getOne } = require('../config/database');
|
||||||
|
const logger = require('../config/logger');
|
||||||
|
|
||||||
|
async function checkTimezone() {
|
||||||
|
try {
|
||||||
|
console.log('\n=== DIAGNOSTIC TIMEZONE CALENDRIER ===\n');
|
||||||
|
|
||||||
|
// 1. Vérifier la timezone de PostgreSQL
|
||||||
|
const pgTimezone = await getOne('SHOW timezone');
|
||||||
|
console.log('1. Timezone PostgreSQL:', pgTimezone.timezone);
|
||||||
|
|
||||||
|
// 2. Vérifier la timezone système Node.js
|
||||||
|
const nodeTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
console.log('2. Timezone Node.js:', nodeTimezone);
|
||||||
|
console.log('3. TZ env var:', process.env.TZ || 'non défini');
|
||||||
|
|
||||||
|
// 3. Récupérer un événement de test
|
||||||
|
const event = await getOne(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
all_day,
|
||||||
|
pg_typeof(start_time) as type_start
|
||||||
|
FROM calendar_events
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
console.log('\n❌ Aucun événement trouvé dans la base de données');
|
||||||
|
console.log(' Veuillez synchroniser avec Google Calendar d\'abord\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n4. Événement récupéré:');
|
||||||
|
console.log(' - ID:', event.id);
|
||||||
|
console.log(' - Titre:', event.title);
|
||||||
|
console.log(' - Type SQL de start_time:', event.type_start);
|
||||||
|
console.log(' - start_time (brut):', event.start_time);
|
||||||
|
console.log(' - Type JS de start_time:', typeof event.start_time);
|
||||||
|
console.log(' - end_time (brut):', event.end_time);
|
||||||
|
console.log(' - Type JS de end_time:', typeof event.end_time);
|
||||||
|
console.log(' - all_day:', event.all_day);
|
||||||
|
|
||||||
|
// 4. Tester la conversion en Date
|
||||||
|
if (typeof event.start_time === 'string') {
|
||||||
|
const dateObj = new Date(event.start_time);
|
||||||
|
console.log('\n5. Conversion en Date object:');
|
||||||
|
console.log(' - new Date(start_time):', dateObj);
|
||||||
|
console.log(' - toISOString():', dateObj.toISOString());
|
||||||
|
console.log(' - toLocaleString("fr-FR"):', dateObj.toLocaleString('fr-FR'));
|
||||||
|
console.log(' - toLocaleString("fr-FR", {timeZone: "Europe/Paris"}):',
|
||||||
|
dateObj.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }));
|
||||||
|
console.log(' - Offset timezone (minutes):', dateObj.getTimezoneOffset());
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ WARNING: start_time n\'est PAS une string!');
|
||||||
|
console.log(' Le types.setTypeParser(1184) ne fonctionne pas correctement');
|
||||||
|
console.log(' Le container doit être rebuild avec --no-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Récupérer la valeur brute SQL
|
||||||
|
const rawEvent = await getOne(`
|
||||||
|
SELECT
|
||||||
|
start_time::text as start_text,
|
||||||
|
start_time AT TIME ZONE 'UTC' as start_utc,
|
||||||
|
start_time AT TIME ZONE 'Europe/Paris' as start_paris
|
||||||
|
FROM calendar_events
|
||||||
|
WHERE id = $1
|
||||||
|
`, [event.id]);
|
||||||
|
|
||||||
|
console.log('\n6. Valeurs SQL brutes:');
|
||||||
|
console.log(' - start_time::text:', rawEvent.start_text);
|
||||||
|
console.log(' - start_time AT TIME ZONE \'UTC\':', rawEvent.start_utc);
|
||||||
|
console.log(' - start_time AT TIME ZONE \'Europe/Paris\':', rawEvent.start_paris);
|
||||||
|
|
||||||
|
// 6. Compter les événements
|
||||||
|
const count = await getOne('SELECT COUNT(*) as count FROM calendar_events');
|
||||||
|
console.log('\n7. Nombre total d\'événements:', count.count);
|
||||||
|
|
||||||
|
console.log('\n=== FIN DIAGNOSTIC ===\n');
|
||||||
|
|
||||||
|
// Recommandations
|
||||||
|
if (typeof event.start_time !== 'string') {
|
||||||
|
console.log('⚠️ ACTION REQUISE:');
|
||||||
|
console.log(' 1. Rebuild le container avec --no-cache:');
|
||||||
|
console.log(' docker-compose build --no-cache notes-app');
|
||||||
|
console.log(' 2. Restart le container:');
|
||||||
|
console.log(' docker-compose restart notes-app');
|
||||||
|
console.log(' 3. Resynchroniser le calendrier depuis l\'interface web\n');
|
||||||
|
} else {
|
||||||
|
console.log('✓ types.setTypeParser fonctionne correctement');
|
||||||
|
console.log(' Les dates sont retournées comme strings\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Erreur:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTimezone();
|
||||||
Reference in new issue
Block a user