diff --git a/config/database.js b/config/database.js index b145772..dd7f80f 100644 --- a/config/database.js +++ b/config/database.js @@ -3,12 +3,27 @@ const { Pool, types } = require('pg'); const bcrypt = require('bcrypt'); const logger = require('./logger'); -// IMPORTANT: Désactiver le parsing automatique des TIMESTAMPTZ en objets Date -// Pour éviter les problèmes de timezone, on renvoie les dates comme strings ISO +// IMPORTANT: Parser personnalisé pour TIMESTAMPTZ +// On force PostgreSQL à renvoyer en UTC (options: '-c timezone=UTC') +// Mais PostgreSQL peut renvoyer "2024-11-17 09:20:00" sans le 'Z' +// On doit normaliser en ISO UTC propre types.setTypeParser(1184, function(stringValue) { // 1184 = TIMESTAMPTZ - // Renvoyer la string ISO au lieu d'un objet Date - return stringValue; + if (!stringValue) return null; + + // 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' + + // Si déjà au format ISO avec Z ou timezone + if (stringValue.includes('Z') || stringValue.match(/[+-]\d{2}:\d{2}$/)) { + return new Date(stringValue).toISOString(); + } + + // 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'; + return new Date(isoString).toISOString(); }); // Configuration PostgreSQL depuis DATABASE_URL ou variables d'environnement @@ -21,6 +36,9 @@ const pool = new Pool({ max: 20, // Maximum de connexions dans le pool idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, + // IMPORTANT: Forcer le timezone à UTC pour toutes les connexions + // Cela garantit que PostgreSQL renvoie toujours les dates en UTC + options: '-c timezone=UTC' }); // Gestion des erreurs du pool diff --git a/scripts/debug-sync.js b/scripts/debug-sync.js new file mode 100644 index 0000000..a2ed49d --- /dev/null +++ b/scripts/debug-sync.js @@ -0,0 +1,144 @@ +// Script de debug pour comprendre le flux de synchronisation +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 || 'localhost'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`; + +const pool = new Pool({ + connectionString: DATABASE_URL +}); + +async function debug() { + console.log('═══════════════════════════════════════════════════════════'); + console.log('🔍 DEBUG SYNCHRONISATION CALENDRIER'); + console.log('═══════════════════════════════════════════════════════════\n'); + + try { + // 1. Récupérer l'événement problématique + const result = await pool.query(` + SELECT + title, + start_time, + pg_typeof(start_time) as type_colonne, + start_time::text as start_text, + to_char(start_time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') as start_iso_format, + start_time AT TIME ZONE 'UTC' as start_utc, + start_time AT TIME ZONE 'Europe/Paris' as start_paris + FROM calendar_events + WHERE title LIKE '%Wlodarczak%' OR title LIKE '%Julian%' + ORDER BY start_time DESC + LIMIT 1 + `); + + if (result.rows.length === 0) { + console.log('❌ Événement "Wlodarczak" non trouvé dans la base'); + console.log(''); + + // Afficher tous les événements + const allEvents = await pool.query(` + SELECT id, title, start_time + FROM calendar_events + ORDER BY start_time DESC + LIMIT 5 + `); + + console.log('Derniers événements dans la base:'); + allEvents.rows.forEach(e => { + console.log(` - ${e.title}: ${e.start_time}`); + }); + + await pool.end(); + return; + } + + const event = result.rows[0]; + + console.log('1️⃣ ÉVÉNEMENT DANS POSTGRESQL:'); + console.log(' Titre:', event.title); + console.log(' Type de colonne:', event.type_colonne); + console.log(' start_time (brut):', event.start_time); + console.log(' start_time::text:', event.start_text); + console.log(' Format ISO:', event.start_iso_format); + console.log(' AT TIME ZONE UTC:', event.start_utc); + console.log(' AT TIME ZONE Europe/Paris:', event.start_paris); + console.log(''); + + // 2. Simuler ce que reçoit le frontend + console.log('2️⃣ CE QUE LE FRONTEND REÇOIT:'); + const apiResult = await pool.query(` + SELECT id, title, start_time, end_time + FROM calendar_events + WHERE title LIKE '%Wlodarczak%' OR title LIKE '%Julian%' + ORDER BY start_time DESC + LIMIT 1 + `); + + console.log(' JSON API:', JSON.stringify(apiResult.rows[0], null, 2)); + console.log(' Type de start_time:', typeof apiResult.rows[0].start_time); + console.log(''); + + // 3. Simuler ce que fait le frontend + console.log('3️⃣ SIMULATION FRONTEND:'); + const startTime = apiResult.rows[0].start_time; + console.log(' start_time reçu:', startTime, '(type:', typeof startTime + ')'); + + const dateObj = new Date(startTime); + console.log(' new Date(start_time):', dateObj); + console.log(' .toISOString():', dateObj.toISOString()); + console.log(' .toString():', dateObj.toString()); + + const parisTime = dateObj.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Paris' + }); + console.log(' .toLocaleTimeString(...Europe/Paris):', parisTime); + console.log(''); + + // 4. Ce que devrait être l'heure + console.log('4️⃣ VÉRIFICATION:'); + console.log(' Google Calendar dit: 10:20'); + console.log(' NoteFlow affiche:', parisTime); + if (parisTime === '10:20') { + console.log(' ✅ CORRECT'); + } else { + console.log(' ❌ INCORRECT - Décalage de', Math.abs(10*60+20 - (parseInt(parisTime.split(':')[0])*60 + parseInt(parisTime.split(':')[1]))), 'minutes'); + } + console.log(''); + + // 5. Test avec différentes conversions + console.log('5️⃣ TESTS DE CONVERSION:'); + + // Test si c'est stocké en UTC + if (typeof startTime === 'string') { + console.log(' String PostgreSQL:', startTime); + + // Si format "2024-11-17 09:20:00" + if (!startTime.includes('T') && !startTime.includes('Z') && !startTime.includes('+')) { + console.log(' ⚠️ Format sans timezone détecté!'); + console.log(' new Date() va interpréter comme heure LOCALE du serveur'); + + // Essayer de construire une date ISO UTC + const isoUTC = startTime.replace(' ', 'T') + 'Z'; + console.log(' Tentative avec "Z": new Date("' + isoUTC + '")'); + const dateUTC = new Date(isoUTC); + const parisUTC = dateUTC.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Paris' + }); + console.log(' → Affichage Paris:', parisUTC); + } + } + + console.log(''); + console.log('═══════════════════════════════════════════════════════════'); + + } catch (error) { + console.error('❌ Erreur:', error.message); + } finally { + await pool.end(); + } +} + +debug(); diff --git a/scripts/test-parser.js b/scripts/test-parser.js new file mode 100644 index 0000000..4b02315 --- /dev/null +++ b/scripts/test-parser.js @@ -0,0 +1,102 @@ +// Test du parser PostgreSQL +console.log('═══════════════════════════════════════════════════════════'); +console.log('🧪 TEST DU PARSER POSTGRESQL'); +console.log('═══════════════════════════════════════════════════════════\n'); + +// Simuler le parser +function parseTimestampTZ(stringValue) { + if (!stringValue) return null; + + // 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' + + // Si déjà au format ISO avec Z ou timezone (+HH:MM ou +HH) + if (stringValue.includes('Z') || stringValue.match(/[+-]\d{2}(:\d{2})?$/)) { + return new Date(stringValue).toISOString(); + } + + // 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'; + return new Date(isoString).toISOString(); +} + +// Test 1: Format PostgreSQL sans timezone (le plus courant) +console.log('Test 1: PostgreSQL format sans timezone'); +const input1 = '2024-11-17 09:20:00'; +const output1 = parseTimestampTZ(input1); +console.log(' Input:', input1); +console.log(' Output:', output1); +console.log(' ✓ Attendu: 2024-11-17T09:20:00.000Z'); +console.log(' ✓ Match:', output1 === '2024-11-17T09:20:00.000Z'); +console.log(''); + +// Test 2: Avec timezone +00 +console.log('Test 2: PostgreSQL avec timezone +00'); +const input2 = '2024-11-17 09:20:00+00'; +const output2 = parseTimestampTZ(input2); +console.log(' Input:', input2); +console.log(' Output:', output2); +console.log(' ✓ Attendu: 2024-11-17T09:20:00.000Z'); +console.log(''); + +// Test 3: Déjà en ISO avec Z +console.log('Test 3: Déjà en ISO UTC'); +const input3 = '2024-11-17T09:20:00Z'; +const output3 = parseTimestampTZ(input3); +console.log(' Input:', input3); +console.log(' Output:', output3); +console.log(' ✓ Attendu: 2024-11-17T09:20:00.000Z'); +console.log(''); + +// Test 4: Affichage frontend +console.log('Test 4: Affichage frontend avec toLocaleTimeString'); +const parsed = parseTimestampTZ('2024-11-17 09:20:00'); +const date = new Date(parsed); +const parisTime = date.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Paris' +}); +console.log(' Input PostgreSQL: 2024-11-17 09:20:00 (09:20 UTC)'); +console.log(' Après parser:', parsed); +console.log(' new Date():', date); +console.log(' Affiché en Paris:', parisTime); +console.log(' ✓ Attendu: 10:20 (09:20 UTC = 10:20 Paris en hiver)'); +console.log(' ✓ Match:', parisTime === '10:20'); +console.log(''); + +// Test 5: Flux complet Google → PostgreSQL → Frontend +console.log('Test 5: Flux complet Google Calendar → Frontend'); +console.log(' 1. Google Calendar envoie: "2024-11-17T10:20:00+01:00" (10:20 Paris)'); + +const googleDate = new Date('2024-11-17T10:20:00+01:00'); +console.log(' 2. Converti en UTC:', googleDate.toISOString(), '(09:20 UTC)'); + +const pgStored = '2024-11-17 09:20:00'; // Ce que PostgreSQL stocke en UTC +console.log(' 3. PostgreSQL stocke:', pgStored, '(format sans Z)'); + +const pgParsed = parseTimestampTZ(pgStored); +console.log(' 4. Parser renvoie:', pgParsed); + +const frontendDate = new Date(pgParsed); +const frontendDisplay = frontendDate.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Paris' +}); +console.log(' 5. Frontend affiche:', frontendDisplay); + +if (frontendDisplay === '10:20') { + console.log(' ✅ SUCCÈS: L\'heure affichée correspond à Google Calendar!'); +} else { + console.log(' ❌ ÉCHEC: Décalage détecté'); + console.log(' Attendu: 10:20'); + console.log(' Obtenu:', frontendDisplay); +} +console.log(''); + +console.log('═══════════════════════════════════════════════════════════'); +console.log('✅ Tests terminés'); +console.log('═══════════════════════════════════════════════════════════');