Merge pull request #106 from R0m1k3/claude/fix-time-offset-bug-01FzrcjKh9o5emsU6hRMMLRL

Add: Système de logs visibles dans le navigateur (sans accès Docker)
This commit is contained in:
LogiFlow authored and GitHub committed 2025-11-16 18:38:08 +01:00
commit f529d09001
6 files changed
+594 -12

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
# 🔍 VOIR LES LOGS DE TIMEZONE DIRECTEMENT DANS VOTRE NAVIGATEUR
Ce système vous permet de voir en temps réel ce qui se passe avec les dates, **sans avoir besoin d'accès aux logs Docker**.
---
## 🚀 COMMENT UTILISER
### **ÉTAPE 1 : Rebuild et redémarrage**
```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 : Ouvrir la page de logs**
Ouvrez votre navigateur et allez sur :
```
http://localhost:2222/api/timezone-logs/html
```
**Laissez cette page ouverte** dans un onglet.
### **ÉTAPE 4 : Synchroniser avec Google Calendar**
1. Dans un autre onglet, ouvrez http://localhost:2222
2. Allez dans **Admin** → **Google Calendar**
3. Cliquez sur **🔄 Synchroniser**
### **ÉTAPE 5 : Regarder les logs apparaître**
Retournez sur l'onglet des logs (`/api/timezone-logs/html`) et vous verrez :
- 🟣 **SYNC** : Ce que Google Calendar renvoie
- 🟠 **PARSER** : Comment PostgreSQL parse les dates
- 🟢 **GET** : Ce qui est envoyé au frontend
---
## 📊 FONCTIONNALITÉS DE LA PAGE DE LOGS
### **Boutons en haut** :
- **🔄 Rafraîchir** : Recharger la page pour voir les nouveaux logs
- **🗑️ Vider les logs** : Supprimer tous les logs
- **⏱️ Auto-refresh** : Activer le rafraîchissement automatique toutes les 3 secondes
- **Filtres** : Afficher seulement les logs d'une catégorie (Tous, Sync, Parser, Get)
### **Statistiques affichées** :
- Total de logs
- Nombre de logs par catégorie
### **Chaque log affiche** :
- ⏰ Timestamp (date et heure exacte)
- 🏷️ Catégorie (SYNC, PARSER, GET)
- 📝 Message descriptif
- 📦 Données JSON (si disponibles)
---
## 🔍 EXEMPLE DE LOGS ATTENDUS
### **Lors de la synchronisation** :
```
[SYNC] 📅 Événement: "Dr Julian Wlodarczak"
{
"googleStartBrut": {
"dateTime": "2024-11-17T10:20:00+01:00"
},
"startTimeExtrait": "2024-11-17T10:20:00+01:00",
"type": "string",
"isAllDay": false
}
[SYNC] → Conversion: new Date("2024-11-17T10:20:00+01:00") = 2024-11-17T09:20:00.000Z
{
"affichageParis": "17/11/2024 10:20:00",
"heureParisSeule": "10:20"
}
```
### **Lors du parsing PostgreSQL** :
```
[PARSER] Input sans TZ: "2024-11-17 09:20:00" → ISO+Z: "2024-11-17T09:20:00Z" → Output: "2024-11-17T09:20:00.000Z"
```
### **Lors de l'envoi au frontend** :
```
[GET] 📤 Envoi au frontend: "Dr Julian Wlodarczak"
{
"start_time_DB": "2024-11-17T09:20:00.000Z",
"type": "string"
}
[GET] → Frontend recevra: 2024-11-17T09:20:00.000Z
{
"apresNewDate": "2024-11-17T09:20:00.000Z",
"affichageParisAttendu": "10:20"
}
```
---
## ✅ CE QU'IL FAUT VÉRIFIER
Dans les logs, vérifiez que :
1. ✅ **Google renvoie** : `"2024-11-17T10:20:00+01:00"` (10:20 heure de Paris)
2. ✅ **Conversion UTC** : `2024-11-17T09:20:00.000Z` (09:20 UTC = 10:20 Paris)
3. ✅ **PostgreSQL stocke** : `"2024-11-17 09:20:00"` (09:20 UTC)
4. ✅ **Parser normalise** : `"2024-11-17T09:20:00.000Z"`
5. ✅ **Frontend reçoit** : `"2024-11-17T09:20:00.000Z"`
6. ✅ **Affichage attendu** : `"10:20"`
---
## 🎨 COULEURS DANS L'INTERFACE
- 🟣 **Violet** : Logs de synchronisation (SYNC)
- 🟠 **Orange** : Logs du parser PostgreSQL (PARSER)
- 🟢 **Vert** : Logs d'envoi au frontend (GET)
---
## 📱 ACCESSIBLE DEPUIS PORTAINER
Si vous utilisez Portainer :
1. Ouvrez Portainer
2. Cliquez sur votre container `notes-app`
3. Dans le navigateur, allez sur : `http://localhost:2222/api/timezone-logs/html`
Vous pouvez aussi accéder à la page depuis **n'importe quel appareil** sur le même réseau :
```
http://VOTRE_IP:2222/api/timezone-logs/html
```
---
## 🆘 SI VOUS VOYEZ UN DÉCALAGE
Prenez une **capture d'écran de la page de logs** et cherchez :
- ❌ Où l'heure change de 10:20 à 11:20
- ❌ Quelle étape cause le décalage (SYNC, PARSER, ou GET)
Envoyez-moi la capture et je pourrai corriger le problème exact.
---
## 🔄 AUTRES ENDPOINTS DISPONIBLES
### **Logs en JSON** :
```
GET http://localhost:2222/api/timezone-logs
```
### **Vider les logs** :
```
POST http://localhost:2222/api/timezone-logs/clear
```
### **Logs d'une catégorie** :
```
GET http://localhost:2222/api/timezone-logs/category/SYNC
GET http://localhost:2222/api/timezone-logs/category/PARSER
GET http://localhost:2222/api/timezone-logs/category/GET
```
---
## 💡 ASTUCE
Activez l'**Auto-refresh** pour voir les logs apparaître en temps réel pendant que vous synchronisez !
+3 -2
View File
@@ -2,6 +2,7 @@
const { Pool, types } = require('pg');
const bcrypt = require('bcrypt');
const logger = require('./logger');
const timezoneLogger = require('../services/timezone-logger');
// IMPORTANT: Parser personnalisé pour TIMESTAMPTZ
// On force PostgreSQL à renvoyer en UTC (options: '-c timezone=UTC')
@@ -22,14 +23,14 @@ types.setTypeParser(1184, function(stringValue) {
// Si déjà au format ISO avec Z ou timezone (+HH:MM ou +HH)
if (stringValue.includes('Z') || stringValue.match(/[+-]\d{2}(:\d{2})?$/)) {
result = new Date(stringValue).toISOString();
logger.debug(`[PARSER TIMESTAMPTZ] Input avec TZ: "${originalValue}" → Output: "${result}"`);
timezoneLogger.log('PARSER', `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}"`);
timezoneLogger.log('PARSER', `Input sans TZ: "${originalValue}" → ISO+Z: "${isoString}" → Output: "${result}"`);
}
return result;
+21 -10
View File
@@ -5,6 +5,7 @@ const { google } = require('googleapis');
const { getAll, getOne, runQuery } = require('../config/database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const logger = require('../config/logger');
const timezoneLogger = require('../services/timezone-logger');
// Scopes Google Calendar requis (lecture ET écriture)
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
@@ -354,12 +355,17 @@ router.get('/events', authenticateToken, async (req, res) => {
// 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})`);
timezoneLogger.log('GET', `📤 Envoi au frontend: "${event.title}"`, {
start_time_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' })}`);
timezoneLogger.log('GET', ` → Frontend recevra: ${event.start_time}`, {
apresNewDate: date.toISOString(),
affichageParisAttendu: date.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })
});
}
});
@@ -503,14 +509,19 @@ router.post('/sync', authenticateToken, async (req, res) => {
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}`);
timezoneLogger.log('SYNC', `📅 Événement: "${event.summary}"`, {
googleStartBrut: event.start,
startTimeExtrait: startTime,
type: typeof startTime,
isAllDay
});
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' })}`);
timezoneLogger.log('SYNC', ` → Conversion: new Date("${startTime}") = ${testDate.toISOString()}`, {
affichageParis: testDate.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }),
heureParisSeule: testDate.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' })
});
}
// Vérifier si l'événement existe déjà
+331
View File
@@ -0,0 +1,331 @@
// Route pour afficher les logs de timezone
const express = require('express');
const router = express.Router();
const { authenticateToken } = require('../middleware/auth');
const timezoneLogger = require('../services/timezone-logger');
/**
* GET /api/timezone-logs
* Afficher tous les logs de timezone
*/
router.get('/', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 100;
const logs = timezoneLogger.getLogs(limit);
res.json({
total: logs.length,
limit,
logs
});
});
/**
* GET /api/timezone-logs/category/:category
* Afficher les logs d'une catégorie spécifique
*/
router.get('/category/:category', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 50;
const logs = timezoneLogger.getLogsByCategory(req.category.toUpperCase(), limit);
res.json({
category: req.params.category,
total: logs.length,
limit,
logs
});
});
/**
* POST /api/timezone-logs/clear
* Vider tous les logs
*/
router.post('/clear', authenticateToken, (req, res) => {
timezoneLogger.clearLogs();
res.json({ message: 'Logs vidés avec succès' });
});
/**
* GET /api/timezone-logs/html
* Afficher les logs en HTML (pour voir directement dans le navigateur)
*/
router.get('/html', authenticateToken, (req, res) => {
const limit = parseInt(req.query.limit) || 200;
const logs = timezoneLogger.getLogs(limit);
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Logs Timezone - NoteFlow</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0f172a;
color: #e2e8f0;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.header h1 {
color: white;
font-size: 32px;
margin-bottom: 10px;
}
.header .stats {
display: flex;
gap: 20px;
margin-top: 15px;
flex-wrap: wrap;
}
.stat {
background: rgba(255,255,255,0.2);
padding: 10px 20px;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: white;
}
.stat-label {
font-size: 12px;
opacity: 0.9;
color: white;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
background: #3b82f6;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
}
button:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
button.danger {
background: #ef4444;
}
button.danger:hover {
background: #dc2626;
}
button.success {
background: #10b981;
}
button.success:hover {
background: #059669;
}
.log-entry {
background: #1e293b;
border-left: 4px solid #3b82f6;
padding: 15px;
margin-bottom: 12px;
border-radius: 8px;
transition: all 0.2s;
}
.log-entry:hover {
background: #2d3b52;
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.log-entry.SYNC {
border-left-color: #8b5cf6;
}
.log-entry.PARSER {
border-left-color: #f59e0b;
}
.log-entry.GET {
border-left-color: #10b981;
}
.log-timestamp {
color: #64748b;
font-size: 12px;
font-family: 'Courier New', monospace;
}
.log-category {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
margin-left: 10px;
text-transform: uppercase;
}
.log-category.SYNC {
background: #8b5cf6;
color: white;
}
.log-category.PARSER {
background: #f59e0b;
color: white;
}
.log-category.GET {
background: #10b981;
color: white;
}
.log-message {
margin: 10px 0;
font-size: 14px;
line-height: 1.6;
color: #e2e8f0;
}
.log-data {
background: #0f172a;
padding: 12px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
margin-top: 10px;
border: 1px solid #334155;
}
.log-data pre {
margin: 0;
color: #94a3b8;
}
.no-logs {
text-align: center;
padding: 60px 20px;
color: #64748b;
}
.filter-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
padding: 8px 16px;
font-size: 13px;
background: #334155;
}
.filter-btn.active {
background: #3b82f6;
}
</style>
</head>
<body>
<div class="header">
<h1>🔍 Logs de débogage Timezone</h1>
<p style="opacity: 0.9; margin-top: 10px;">Suivi en temps réel des conversions de dates</p>
<div class="stats">
<div class="stat">
<div class="stat-value">${logs.length}</div>
<div class="stat-label">Total logs</div>
</div>
<div class="stat">
<div class="stat-value">${logs.filter(l => l.category === 'SYNC').length}</div>
<div class="stat-label">Synchronisation</div>
</div>
<div class="stat">
<div class="stat-value">${logs.filter(l => l.category === 'PARSER').length}</div>
<div class="stat-label">Parser</div>
</div>
<div class="stat">
<div class="stat-value">${logs.filter(l => l.category === 'GET').length}</div>
<div class="stat-label">Frontend</div>
</div>
</div>
</div>
<div class="controls">
<button onclick="location.reload()" class="success">🔄 Rafraîchir</button>
<button onclick="clearLogs()" class="danger">🗑️ Vider les logs</button>
<button onclick="autoRefresh()" id="autoRefreshBtn">⏱️ Auto-refresh OFF</button>
<div class="filter-buttons" style="margin-left: auto;">
<button class="filter-btn active" onclick="filterLogs('ALL')">Tous</button>
<button class="filter-btn" onclick="filterLogs('SYNC')">Sync</button>
<button class="filter-btn" onclick="filterLogs('PARSER')">Parser</button>
<button class="filter-btn" onclick="filterLogs('GET')">Get</button>
</div>
</div>
<div id="logs-container">
${logs.length === 0 ? '<div class="no-logs">Aucun log pour le moment. Synchronisez avec Google Calendar pour voir les logs.</div>' : ''}
${logs.map(log => `
<div class="log-entry ${log.category}" data-category="${log.category}">
<div>
<span class="log-timestamp">${new Date(log.timestamp).toLocaleString('fr-FR')}</span>
<span class="log-category ${log.category}">${log.category}</span>
</div>
<div class="log-message">${log.message}</div>
${log.data ? `<div class="log-data"><pre>${log.data}</pre></div>` : ''}
</div>
`).join('')}
</div>
<script>
let autoRefreshInterval = null;
function filterLogs(category) {
const entries = document.querySelectorAll('.log-entry');
const buttons = document.querySelectorAll('.filter-btn');
buttons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
entries.forEach(entry => {
if (category === 'ALL' || entry.dataset.category === category) {
entry.style.display = 'block';
} else {
entry.style.display = 'none';
}
});
}
function clearLogs() {
if (confirm('Voulez-vous vraiment vider tous les logs ?')) {
fetch('/api/timezone-logs/clear', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}).then(() => location.reload());
}
}
function autoRefresh() {
const btn = document.getElementById('autoRefreshBtn');
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
btn.textContent = '⏱️ Auto-refresh OFF';
btn.style.background = '#3b82f6';
} else {
autoRefreshInterval = setInterval(() => location.reload(), 3000);
btn.textContent = '⏱️ Auto-refresh ON (3s)';
btn.style.background = '#10b981';
}
}
</script>
</body>
</html>
`;
res.send(html);
});
module.exports = router;
+1
View File
@@ -93,6 +93,7 @@ app.use('/api/settings', require('./routes/settings.routes'));
app.use('/api/rss', require('./routes/rss.routes'));
app.use('/api/calendar', require('./routes/calendar.routes'));
app.use('/api/openrouter', require('./routes/openrouter.routes'));
app.use('/api/timezone-logs', require('./routes/timezone-logs.routes'));
// Route de recherche
app.get('/api/search', require('./routes/notes.routes').searchNotes);
+48
View File
@@ -0,0 +1,48 @@
// Service de logging en mémoire pour débuguer le timezone
// Les logs sont stockés en mémoire et accessibles via /api/calendar/timezone-logs
class TimezoneLogger {
constructor() {
this.logs = [];
this.maxLogs = 500; // Garder max 500 logs
}
log(category, message, data = null) {
const entry = {
timestamp: new Date().toISOString(),
category,
message,
data: data ? JSON.stringify(data, null, 2) : null
};
this.logs.push(entry);
// Limiter le nombre de logs
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Afficher aussi dans les logs console
console.log(`[${category}] ${message}`, data || '');
}
getLogs(limit = 100) {
return this.logs.slice(-limit).reverse();
}
clearLogs() {
this.logs = [];
}
getLogsByCategory(category, limit = 50) {
return this.logs
.filter(log => log.category === category)
.slice(-limit)
.reverse();
}
}
// Singleton
const timezoneLogger = new TimezoneLogger();
module.exports = timezoneLogger;