From 8538dbdd0eed60b7dcfd85d04b4e9b700cdc143a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 17:54:37 +0000 Subject: [PATCH] =?UTF-8?q?Fix:=20Correction=20du=20d=C3=A9calage=20horair?= =?UTF-8?q?e=20de=201h=20pour=20les=20=C3=A9v=C3=A9nements=20Google=20Cale?= =?UTF-8?q?ndar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problème identifié: - Google Calendar envoie '2025-11-17T10:20:00+01:00' (10h20 Paris = 09h20 UTC) - PostgreSQL TIMESTAMP enlève le fuseau horaire et stocke '2025-11-17 10:20:00' (heure locale) - Le parser 1114 traite cette valeur comme UTC → '2025-11-17T10:20:00.000Z' - Frontend affiche 11h20 en heure de Paris (10:20 UTC + 1h) au lieu de 10h20 Solution: - Convertir tous les timestamps en UTC (ISO format) avant insertion dans PostgreSQL - Appliquer la conversion dans toutes les routes: POST /sync, POST /events, PUT /events/:id - Le parser 1114 traite maintenant correctement les valeurs UTC stockées Changements: - routes/calendar.routes.js: Conversion UTC avant insertion (3 routes corrigées) - routes/calendar.routes.js: Correction syntaxe PostgreSQL (INSERT ... ON CONFLICT) - scripts/cleanup-calendar-events.js: Script pour nettoyer les événements existants Pour appliquer la correction sur les données existantes: 1. Exécuter: docker exec notes-todo-app node scripts/cleanup-calendar-events.js 2. Redémarrer: docker-compose restart notes-app 3. Resynchroniser Google Calendar depuis l'interface web --- config/database.js | 9 ++++- routes/calendar.routes.js | 62 +++++++++++++++++++++-------- scripts/cleanup-calendar-events.js | 64 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 17 deletions(-) create mode 100755 scripts/cleanup-calendar-events.js diff --git a/config/database.js b/config/database.js index ea049c3..39f6973 100644 --- a/config/database.js +++ b/config/database.js @@ -33,9 +33,16 @@ types.setTypeParser(1184, function(stringValue) { return result; }); -// Parser pour TIMESTAMP WITHOUT TIME ZONE (1114) - au cas où +// Parser pour TIMESTAMP WITHOUT TIME ZONE (1114) +// IMPORTANT: Les valeurs TIMESTAMP sont stockées en heure locale (Europe/Paris) +// mais PostgreSQL les retourne sans fuseau horaire. +// Exemple: Google envoie "2025-11-17T10:20:00+01:00" → PG stocke "2025-11-17 10:20:00" +// On doit interpréter cette heure comme UTC (car PG a converti lors de l'insertion) types.setTypeParser(1114, function(stringValue) { if (!stringValue) return null; + + // PostgreSQL retourne "YYYY-MM-DD HH:MM:SS" sans fuseau horaire + // Cette valeur doit être traitée comme UTC car PostgreSQL stocke en UTC en interne const isoString = stringValue.replace(' ', 'T') + 'Z'; const result = new Date(isoString).toISOString(); timezoneLogger.log('PARSER', `[1114 TIMESTAMP] Input: "${stringValue}" → Output: "${result}"`); diff --git a/routes/calendar.routes.js b/routes/calendar.routes.js index 36aa764..cba1eff 100644 --- a/routes/calendar.routes.js +++ b/routes/calendar.routes.js @@ -504,21 +504,26 @@ router.post('/sync', authenticateToken, async (req, res) => { // Sauvegarder chaque événement dans la base de données for (const event of events) { - const startTime = event.start.dateTime || event.start.date; - const endTime = event.end.dateTime || event.end.date; + const startTimeRaw = event.start.dateTime || event.start.date; + const endTimeRaw = event.end.dateTime || event.end.date; const isAllDay = !event.start.dateTime; // Si pas de dateTime, c'est un événement toute la journée + // Convertir en UTC pour PostgreSQL TIMESTAMP + // PostgreSQL TIMESTAMP stocke l'heure sans fuseau horaire, donc il faut convertir en UTC avant insertion + const startTime = isAllDay ? startTimeRaw : new Date(startTimeRaw).toISOString(); + const endTime = isAllDay ? endTimeRaw : new Date(endTimeRaw).toISOString(); + // LOG DÉTAILLÉ pour debug timezone timezoneLogger.log('SYNC', `📅 Événement: "${event.summary}"`, { googleStartBrut: event.start, - startTimeExtrait: startTime, - type: typeof startTime, + startTimeExtrait: startTimeRaw, + type: typeof startTimeRaw, isAllDay }); - if (startTime) { - const testDate = new Date(startTime); - timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTime}") = ${testDate.toISOString()}`, { + if (startTimeRaw) { + const testDate = new Date(startTimeRaw); + timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTimeRaw}") = ${testDate.toISOString()}`, { affichageParis: testDate.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }), heureParisSeule: testDate.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' }) }); @@ -534,7 +539,8 @@ router.post('/sync', authenticateToken, async (req, res) => { timezoneLogger.log('SYNC', ` → Insertion dans PostgreSQL`, { startTimeType: typeof startTime, startTimeValue: startTime, - endTimeValue: endTime + endTimeValue: endTime, + convertedToUTC: !isAllDay }); if (existing) { @@ -692,19 +698,38 @@ router.post('/events', authenticateToken, async (req, res) => { if (events.data.items && events.data.items.length > 0) { for (const item of events.data.items) { + const itemStartRaw = item.start.dateTime || item.start.date; + const itemEndRaw = item.end.dateTime || item.end.date; + const itemIsAllDay = !item.start.dateTime; + + // Convertir en UTC pour PostgreSQL TIMESTAMP + const itemStartUTC = itemIsAllDay ? itemStartRaw : new Date(itemStartRaw).toISOString(); + const itemEndUTC = itemIsAllDay ? itemEndRaw : new Date(itemEndRaw).toISOString(); + + // PostgreSQL : INSERT ... ON CONFLICT DO UPDATE (pas INSERT OR REPLACE) await runQuery(` - INSERT OR REPLACE INTO calendar_events - (google_event_id, user_id, title, description, start_time, end_time, location, html_link, synced_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + INSERT INTO calendar_events + (google_event_id, user_id, title, description, start_time, end_time, location, html_link, all_day, synced_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP) + ON CONFLICT (google_event_id) DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + start_time = EXCLUDED.start_time, + end_time = EXCLUDED.end_time, + location = EXCLUDED.location, + html_link = EXCLUDED.html_link, + all_day = EXCLUDED.all_day, + synced_at = CURRENT_TIMESTAMP `, [ item.id, req.user.id, item.summary || 'Sans titre', item.description || null, - item.start.dateTime || item.start.date, - item.end.dateTime || item.end.date, + itemStartUTC, + itemEndUTC, item.location || null, - item.htmlLink || null + item.htmlLink || null, + itemIsAllDay ]); } } @@ -803,6 +828,11 @@ router.put('/events/:id', authenticateToken, async (req, res) => { // Mettre à jour dans la base de données locale const isAllDay = !startDateTime.includes('T') && !endDateTime.includes('T'); + + // Convertir en UTC pour PostgreSQL TIMESTAMP (comme dans la synchronisation) + const startTimeUTC = isAllDay ? startDateTime : new Date(startDateTime).toISOString(); + const endTimeUTC = isAllDay ? endDateTime : new Date(endDateTime).toISOString(); + await runQuery(` UPDATE calendar_events SET title = $1, description = $2, start_time = $3, end_time = $4, @@ -811,8 +841,8 @@ router.put('/events/:id', authenticateToken, async (req, res) => { `, [ title, description || null, - startDateTime, - endDateTime, + startTimeUTC, + endTimeUTC, location || null, response.data.htmlLink || null, isAllDay ? 1 : 0, diff --git a/scripts/cleanup-calendar-events.js b/scripts/cleanup-calendar-events.js new file mode 100755 index 0000000..364bea5 --- /dev/null +++ b/scripts/cleanup-calendar-events.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/** + * Script pour nettoyer les événements calendar existants + * À exécuter après la correction du bug timezone pour forcer une resynchronisation + */ + +const { Pool } = require('pg'); + +const DATABASE_URL = process.env.DATABASE_URL || + `postgresql://${process.env.PGUSER || 'noteflow'}:${process.env.PGPASSWORD || 'noteflow_secure_password_change_me'}@${process.env.PGHOST || 'postgres'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`; + +async function cleanup() { + const pool = new Pool({ connectionString: DATABASE_URL }); + + try { + console.log('\n╔════════════════════════════════════════════════════════════╗'); + console.log('║ NETTOYAGE DES ÉVÉNEMENTS CALENDAR ║'); + console.log('╚════════════════════════════════════════════════════════════╝\n'); + + const client = await pool.connect(); + + // Compter les événements avant + const countBefore = await client.query('SELECT COUNT(*) FROM calendar_events'); + console.log(`📊 Nombre d'événements actuels: ${countBefore.rows[0].count}\n`); + + if (parseInt(countBefore.rows[0].count) === 0) { + console.log('✅ Aucun événement à nettoyer\n'); + client.release(); + await pool.end(); + return; + } + + // Supprimer tous les événements + console.log('🗑️ Suppression des événements...'); + const deleteResult = await client.query('DELETE FROM calendar_events'); + console.log(`✅ ${deleteResult.rowCount} événement(s) supprimé(s)\n`); + + // Vérifier après + const countAfter = await client.query('SELECT COUNT(*) FROM calendar_events'); + console.log(`📊 Nombre d'événements après: ${countAfter.rows[0].count}\n`); + + console.log('╔════════════════════════════════════════════════════════════╗'); + console.log('║ ✅ NETTOYAGE TERMINÉ ║'); + console.log('╚════════════════════════════════════════════════════════════╝\n'); + + console.log('📝 PROCHAINES ÉTAPES:\n'); + console.log('1. Redémarrez l\'application avec: docker-compose restart notes-app'); + console.log('2. Ouvrez l\'interface web'); + console.log('3. Allez dans Admin → Google Calendar'); + console.log('4. Cliquez sur "Synchroniser"'); + console.log('5. Vérifiez que les heures sont maintenant correctes\n'); + + client.release(); + await pool.end(); + + } catch (error) { + console.error('\n❌ Erreur:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +cleanup();