Files
noteflow/services/cleanup-scheduler.js
T
Claude 86e825b9d1 Feat: Ajout du système de purge automatique de la base de données
Implémente un système complet de purge automatique pour maintenir
la base de données propre et performante.

Fonctionnalités ajoutées :
- Migration pour ajouter les champs de tracking (archived_at, completed_at)
- Triggers PostgreSQL pour mise à jour automatique des dates
- Script de purge manuel avec mode simulation (dry-run)
- Scheduler automatique configurable (défaut: 24h)
- Routes API admin pour contrôle et monitoring
- Documentation complète

Éléments purgés :
- Flux RSS désactivés (tous)
- Tâches complétées > 3 mois (configurable)
- Notes archivées > 6 mois (configurable)
- Rendez-vous passés > 6 mois (configurable)

Scripts npm ajoutés :
- npm run db:migrate - Exécuter la migration
- npm run db:cleanup - Purge manuelle
- npm run db:cleanup:dry-run - Simulation de purge

Routes API ajoutées :
- POST /api/admin/cleanup - Déclencher la purge
- GET /api/admin/cleanup/preview - Prévisualiser la purge
- GET /api/admin/cleanup/status - Statut du scheduler
- GET /api/admin/stats - Statistiques DB

Configuration via variables d'environnement :
- CLEANUP_ENABLED (défaut: true)
- CLEANUP_INTERVAL_HOURS (défaut: 24)
- CLEANUP_COMPLETED_TASKS_DAYS (défaut: 90)
- CLEANUP_ARCHIVED_NOTES_DAYS (défaut: 180)
- CLEANUP_PAST_EVENTS_DAYS (défaut: 180)
2025-11-19 06:49:27 +00:00

217 lines
8.1 KiB
JavaScript

// Service de purge automatique de la base de données
const logger = require('../config/logger');
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'}`;
// Configuration
const CLEANUP_INTERVAL = parseInt(process.env.CLEANUP_INTERVAL_HOURS) || 24; // Par défaut, toutes les 24 heures
const STARTUP_DELAY = 60000; // 1 minute après le démarrage
const CLEANUP_ENABLED = process.env.CLEANUP_ENABLED !== 'false'; // Activé par défaut
const CLEANUP_CONFIG = {
COMPLETED_TASKS_DAYS: parseInt(process.env.CLEANUP_COMPLETED_TASKS_DAYS) || 90,
ARCHIVED_NOTES_DAYS: parseInt(process.env.CLEANUP_ARCHIVED_NOTES_DAYS) || 180,
PAST_EVENTS_DAYS: parseInt(process.env.CLEANUP_PAST_EVENTS_DAYS) || 180
};
let cleanupTimer = null;
let isRunning = false;
/**
* Exécuter la purge automatique
*/
async function executeCleanup() {
if (isRunning) {
logger.warn('Purge déjà en cours, passage ignoré');
return;
}
if (!CLEANUP_ENABLED) {
logger.debug('Purge automatique désactivée (CLEANUP_ENABLED=false)');
return;
}
isRunning = true;
const pool = new Pool({ connectionString: DATABASE_URL });
const stats = {
rssFeeds: 0,
globalTodos: 0,
noteTodos: 0,
archivedNotes: 0,
calendarEvents: 0
};
try {
logger.info('═══════════════════════════════════════════════════');
logger.info('🧹 DÉBUT DE LA PURGE AUTOMATIQUE');
logger.info('═══════════════════════════════════════════════════');
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Flux RSS désactivés
const disabledFeeds = await client.query(`
SELECT id, title FROM rss_feeds WHERE enabled = FALSE
`);
if (disabledFeeds.rows.length > 0) {
logger.info(`📡 Suppression de ${disabledFeeds.rows.length} flux RSS désactivé(s)...`);
const deleteFeeds = await client.query('DELETE FROM rss_feeds WHERE enabled = FALSE');
stats.rssFeeds = deleteFeeds.rowCount;
logger.info(` ✓ ${stats.rssFeeds} flux supprimé(s)`);
}
// 2. Tâches globales complétées
const deleteGlobalTodos = await client.query(`
DELETE FROM global_todos
WHERE completed = TRUE
AND completed_at IS NOT NULL
AND completed_at < NOW() - INTERVAL '${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} days'
`);
stats.globalTodos = deleteGlobalTodos.rowCount;
if (stats.globalTodos > 0) {
logger.info(`✅ Suppression de ${stats.globalTodos} tâche(s) globale(s) complétée(s) > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours`);
}
// 3. Tâches de notes complétées
const deleteNoteTodos = await client.query(`
DELETE FROM note_todos
WHERE completed = TRUE
AND completed_at IS NOT NULL
AND completed_at < NOW() - INTERVAL '${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} days'
`);
stats.noteTodos = deleteNoteTodos.rowCount;
if (stats.noteTodos > 0) {
logger.info(`📝 Suppression de ${stats.noteTodos} tâche(s) de note(s) complétée(s) > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours`);
}
// 4. Notes archivées
const deleteNotes = await client.query(`
DELETE FROM notes
WHERE archived = TRUE
AND archived_at IS NOT NULL
AND archived_at < NOW() - INTERVAL '${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} days'
`);
stats.archivedNotes = deleteNotes.rowCount;
if (stats.archivedNotes > 0) {
logger.info(`🗄️ Suppression de ${stats.archivedNotes} note(s) archivée(s) > ${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} jours`);
}
// 5. Rendez-vous passés
const deleteEvents = await client.query(`
DELETE FROM calendar_events
WHERE end_time < NOW() - INTERVAL '${CLEANUP_CONFIG.PAST_EVENTS_DAYS} days'
`);
stats.calendarEvents = deleteEvents.rowCount;
if (stats.calendarEvents > 0) {
logger.info(`📅 Suppression de ${stats.calendarEvents} rendez-vous terminé(s) > ${CLEANUP_CONFIG.PAST_EVENTS_DAYS} jours`);
}
await client.query('COMMIT');
const totalDeleted = stats.rssFeeds + stats.globalTodos + stats.noteTodos + stats.archivedNotes + stats.calendarEvents;
if (totalDeleted > 0) {
logger.info('═══════════════════════════════════════════════════');
logger.info(`✅ Purge terminée: ${totalDeleted} élément(s) supprimé(s)`);
logger.info('═══════════════════════════════════════════════════');
} else {
logger.info('✓ Purge terminée: Aucun élément à supprimer');
}
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
await pool.end();
} catch (error) {
logger.error('❌ Erreur lors de la purge automatique:', error);
} finally {
isRunning = false;
}
}
/**
* Démarrer le scheduler de purge
*/
function startScheduler() {
if (cleanupTimer) {
logger.warn('Scheduler de purge déjà démarré');
return;
}
if (!CLEANUP_ENABLED) {
logger.info('⚠️ Purge automatique désactivée (CLEANUP_ENABLED=false)');
return;
}
logger.info('═══════════════════════════════════════════════════');
logger.info('🧹 DÉMARRAGE DU SCHEDULER DE PURGE AUTOMATIQUE');
logger.info('═══════════════════════════════════════════════════');
logger.info(`Configuration:`);
logger.info(` • Intervalle: toutes les ${CLEANUP_INTERVAL} heure(s)`);
logger.info(` • Tâches complétées: > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours`);
logger.info(` • Notes archivées: > ${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} jours`);
logger.info(` • Rendez-vous passés: > ${CLEANUP_CONFIG.PAST_EVENTS_DAYS} jours`);
logger.info(` • Première exécution: dans ${STARTUP_DELAY / 1000} secondes`);
logger.info('═══════════════════════════════════════════════════');
// Première exécution après le délai de démarrage
setTimeout(() => {
logger.info('🚀 Exécution initiale de la purge...');
executeCleanup();
}, STARTUP_DELAY);
// Exécutions périodiques
const intervalMs = CLEANUP_INTERVAL * 60 * 60 * 1000; // Convertir heures en millisecondes
cleanupTimer = setInterval(() => {
logger.info('⏰ Exécution périodique de la purge...');
executeCleanup();
}, intervalMs);
logger.info('✓ Scheduler de purge démarré');
}
/**
* Arrêter le scheduler de purge
*/
function stopScheduler() {
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
logger.info('✓ Scheduler de purge arrêté');
}
}
/**
* Obtenir le statut du scheduler
*/
function getStatus() {
return {
enabled: CLEANUP_ENABLED,
running: isRunning,
scheduled: !!cleanupTimer,
config: {
intervalHours: CLEANUP_INTERVAL,
completedTasksDays: CLEANUP_CONFIG.COMPLETED_TASKS_DAYS,
archivedNotesDays: CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS,
pastEventsDays: CLEANUP_CONFIG.PAST_EVENTS_DAYS
}
};
}
module.exports = {
startScheduler,
stopScheduler,
executeCleanup,
getStatus
};