mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-19 21:28:33 +02:00
Fix: Correction du décalage horaire de 1h pour les événements Google Calendar
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
This commit is contained in:
3 files changed
+118
-17
No files matched your search
+8
-1
@@ -33,9 +33,16 @@ types.setTypeParser(1184, function(stringValue) {
|
|||||||
return result;
|
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) {
|
types.setTypeParser(1114, function(stringValue) {
|
||||||
if (!stringValue) return null;
|
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 isoString = stringValue.replace(' ', 'T') + 'Z';
|
||||||
const result = new Date(isoString).toISOString();
|
const result = new Date(isoString).toISOString();
|
||||||
timezoneLogger.log('PARSER', `[1114 TIMESTAMP] Input: "${stringValue}" → Output: "${result}"`);
|
timezoneLogger.log('PARSER', `[1114 TIMESTAMP] Input: "${stringValue}" → Output: "${result}"`);
|
||||||
|
|||||||
+46
-16
@@ -504,21 +504,26 @@ router.post('/sync', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
// Sauvegarder chaque événement dans la base de données
|
// Sauvegarder chaque événement dans la base de données
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const startTime = event.start.dateTime || event.start.date;
|
const startTimeRaw = event.start.dateTime || event.start.date;
|
||||||
const endTime = event.end.dateTime || event.end.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
|
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
|
// LOG DÉTAILLÉ pour debug timezone
|
||||||
timezoneLogger.log('SYNC', `📅 Événement: "${event.summary}"`, {
|
timezoneLogger.log('SYNC', `📅 Événement: "${event.summary}"`, {
|
||||||
googleStartBrut: event.start,
|
googleStartBrut: event.start,
|
||||||
startTimeExtrait: startTime,
|
startTimeExtrait: startTimeRaw,
|
||||||
type: typeof startTime,
|
type: typeof startTimeRaw,
|
||||||
isAllDay
|
isAllDay
|
||||||
});
|
});
|
||||||
|
|
||||||
if (startTime) {
|
if (startTimeRaw) {
|
||||||
const testDate = new Date(startTime);
|
const testDate = new Date(startTimeRaw);
|
||||||
timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTime}") = ${testDate.toISOString()}`, {
|
timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTimeRaw}") = ${testDate.toISOString()}`, {
|
||||||
affichageParis: 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' })
|
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`, {
|
timezoneLogger.log('SYNC', ` → Insertion dans PostgreSQL`, {
|
||||||
startTimeType: typeof startTime,
|
startTimeType: typeof startTime,
|
||||||
startTimeValue: startTime,
|
startTimeValue: startTime,
|
||||||
endTimeValue: endTime
|
endTimeValue: endTime,
|
||||||
|
convertedToUTC: !isAllDay
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -692,19 +698,38 @@ router.post('/events', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
if (events.data.items && events.data.items.length > 0) {
|
if (events.data.items && events.data.items.length > 0) {
|
||||||
for (const item of events.data.items) {
|
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(`
|
await runQuery(`
|
||||||
INSERT OR REPLACE INTO calendar_events
|
INSERT INTO calendar_events
|
||||||
(google_event_id, user_id, title, description, start_time, end_time, location, html_link, synced_at)
|
(google_event_id, user_id, title, description, start_time, end_time, location, html_link, all_day, synced_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
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,
|
item.id,
|
||||||
req.user.id,
|
req.user.id,
|
||||||
item.summary || 'Sans titre',
|
item.summary || 'Sans titre',
|
||||||
item.description || null,
|
item.description || null,
|
||||||
item.start.dateTime || item.start.date,
|
itemStartUTC,
|
||||||
item.end.dateTime || item.end.date,
|
itemEndUTC,
|
||||||
item.location || null,
|
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
|
// Mettre à jour dans la base de données locale
|
||||||
const isAllDay = !startDateTime.includes('T') && !endDateTime.includes('T');
|
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(`
|
await runQuery(`
|
||||||
UPDATE calendar_events
|
UPDATE calendar_events
|
||||||
SET title = $1, description = $2, start_time = $3, end_time = $4,
|
SET title = $1, description = $2, start_time = $3, end_time = $4,
|
||||||
@@ -811,8 +841,8 @@ router.put('/events/:id', authenticateToken, async (req, res) => {
|
|||||||
`, [
|
`, [
|
||||||
title,
|
title,
|
||||||
description || null,
|
description || null,
|
||||||
startDateTime,
|
startTimeUTC,
|
||||||
endDateTime,
|
endTimeUTC,
|
||||||
location || null,
|
location || null,
|
||||||
response.data.htmlLink || null,
|
response.data.htmlLink || null,
|
||||||
isAllDay ? 1 : 0,
|
isAllDay ? 1 : 0,
|
||||||
|
|||||||
Executable
+64
@@ -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();
|
||||||
Reference in new issue
Block a user