mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-22 14:48:18 +02:00
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)
This commit is contained in:
8 files changed
+1532
-2
No files matched your search
@@ -0,0 +1,365 @@
|
||||
// Routes d'administration et de maintenance
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||
const logger = require('../config/logger');
|
||||
const { Pool } = require('pg');
|
||||
const cleanupScheduler = require('../services/cleanup-scheduler');
|
||||
|
||||
// Toutes les routes nécessitent authentification admin
|
||||
router.use(authenticateToken);
|
||||
router.use(requireAdmin);
|
||||
|
||||
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'}`;
|
||||
|
||||
/**
|
||||
* POST /api/admin/cleanup
|
||||
* Déclencher la purge de la base de données
|
||||
*
|
||||
* Body (optionnel):
|
||||
* {
|
||||
* "dryRun": true, // Mode simulation (défaut: false)
|
||||
* "config": {
|
||||
* "completedTasksDays": 90,
|
||||
* "archivedNotesDays": 180,
|
||||
* "pastEventsDays": 180
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
router.post('/cleanup', async (req, res) => {
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
const dryRun = req.body.dryRun || false;
|
||||
const config = req.body.config || {
|
||||
completedTasksDays: 90,
|
||||
archivedNotesDays: 180,
|
||||
pastEventsDays: 180
|
||||
};
|
||||
|
||||
const stats = {
|
||||
rssFeeds: 0,
|
||||
rssArticles: 0,
|
||||
globalTodos: 0,
|
||||
noteTodos: 0,
|
||||
archivedNotes: 0,
|
||||
calendarEvents: 0
|
||||
};
|
||||
|
||||
try {
|
||||
logger.info(`Début de la purge de la base de données (DRY_RUN: ${dryRun})`);
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Flux RSS désactivés
|
||||
const disabledFeeds = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM rss_feeds
|
||||
WHERE enabled = FALSE
|
||||
`);
|
||||
|
||||
if (!dryRun && parseInt(disabledFeeds.rows[0].count) > 0) {
|
||||
const deleteFeeds = await client.query(`
|
||||
DELETE FROM rss_feeds
|
||||
WHERE enabled = FALSE
|
||||
`);
|
||||
stats.rssFeeds = deleteFeeds.rowCount;
|
||||
} else {
|
||||
stats.rssFeeds = parseInt(disabledFeeds.rows[0].count);
|
||||
}
|
||||
|
||||
// 2. Tâches globales complétées
|
||||
const oldGlobalTodos = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM global_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
|
||||
if (!dryRun && parseInt(oldGlobalTodos.rows[0].count) > 0) {
|
||||
const deleteGlobalTodos = await client.query(`
|
||||
DELETE FROM global_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
stats.globalTodos = deleteGlobalTodos.rowCount;
|
||||
} else {
|
||||
stats.globalTodos = parseInt(oldGlobalTodos.rows[0].count);
|
||||
}
|
||||
|
||||
// 3. Tâches de notes complétées
|
||||
const oldNoteTodos = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM note_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
|
||||
if (!dryRun && parseInt(oldNoteTodos.rows[0].count) > 0) {
|
||||
const deleteNoteTodos = await client.query(`
|
||||
DELETE FROM note_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
stats.noteTodos = deleteNoteTodos.rowCount;
|
||||
} else {
|
||||
stats.noteTodos = parseInt(oldNoteTodos.rows[0].count);
|
||||
}
|
||||
|
||||
// 4. Notes archivées
|
||||
const oldArchivedNotes = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM notes
|
||||
WHERE archived = TRUE
|
||||
AND archived_at IS NOT NULL
|
||||
AND archived_at < NOW() - INTERVAL '${config.archivedNotesDays} days'
|
||||
`);
|
||||
|
||||
if (!dryRun && parseInt(oldArchivedNotes.rows[0].count) > 0) {
|
||||
const deleteNotes = await client.query(`
|
||||
DELETE FROM notes
|
||||
WHERE archived = TRUE
|
||||
AND archived_at IS NOT NULL
|
||||
AND archived_at < NOW() - INTERVAL '${config.archivedNotesDays} days'
|
||||
`);
|
||||
stats.archivedNotes = deleteNotes.rowCount;
|
||||
} else {
|
||||
stats.archivedNotes = parseInt(oldArchivedNotes.rows[0].count);
|
||||
}
|
||||
|
||||
// 5. Rendez-vous passés
|
||||
const oldEvents = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM calendar_events
|
||||
WHERE end_time < NOW() - INTERVAL '${config.pastEventsDays} days'
|
||||
`);
|
||||
|
||||
if (!dryRun && parseInt(oldEvents.rows[0].count) > 0) {
|
||||
const deleteEvents = await client.query(`
|
||||
DELETE FROM calendar_events
|
||||
WHERE end_time < NOW() - INTERVAL '${config.pastEventsDays} days'
|
||||
`);
|
||||
stats.calendarEvents = deleteEvents.rowCount;
|
||||
} else {
|
||||
stats.calendarEvents = parseInt(oldEvents.rows[0].count);
|
||||
}
|
||||
|
||||
// COMMIT ou ROLLBACK selon le mode
|
||||
if (!dryRun) {
|
||||
await client.query('COMMIT');
|
||||
logger.info('Purge de la base de données terminée avec succès', stats);
|
||||
} else {
|
||||
await client.query('ROLLBACK');
|
||||
logger.info('Simulation de purge terminée', stats);
|
||||
}
|
||||
|
||||
const totalDeleted = stats.rssFeeds + stats.globalTodos + stats.noteTodos + stats.archivedNotes + stats.calendarEvents;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
dryRun,
|
||||
config,
|
||||
stats,
|
||||
total: totalDeleted,
|
||||
message: dryRun
|
||||
? `Simulation terminée: ${totalDeleted} élément(s) seraient supprimés`
|
||||
: `Purge terminée avec succès: ${totalDeleted} élément(s) supprimés`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la purge de la base de données:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur lors de la purge de la base de données',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/cleanup/preview
|
||||
* Prévisualiser ce qui serait supprimé par la purge
|
||||
*/
|
||||
router.get('/cleanup/preview', async (req, res) => {
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
const config = {
|
||||
completedTasksDays: parseInt(req.query.completedTasksDays) || 90,
|
||||
archivedNotesDays: parseInt(req.query.archivedNotesDays) || 180,
|
||||
pastEventsDays: parseInt(req.query.pastEventsDays) || 180
|
||||
};
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
const preview = {};
|
||||
|
||||
try {
|
||||
// Compter les éléments à purger
|
||||
const disabledFeeds = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM rss_feeds
|
||||
WHERE enabled = FALSE
|
||||
`);
|
||||
preview.rssFeeds = parseInt(disabledFeeds.rows[0].count);
|
||||
|
||||
const oldGlobalTodos = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM global_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
preview.globalTodos = parseInt(oldGlobalTodos.rows[0].count);
|
||||
|
||||
const oldNoteTodos = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM note_todos
|
||||
WHERE completed = TRUE
|
||||
AND completed_at IS NOT NULL
|
||||
AND completed_at < NOW() - INTERVAL '${config.completedTasksDays} days'
|
||||
`);
|
||||
preview.noteTodos = parseInt(oldNoteTodos.rows[0].count);
|
||||
|
||||
const oldArchivedNotes = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM notes
|
||||
WHERE archived = TRUE
|
||||
AND archived_at IS NOT NULL
|
||||
AND archived_at < NOW() - INTERVAL '${config.archivedNotesDays} days'
|
||||
`);
|
||||
preview.archivedNotes = parseInt(oldArchivedNotes.rows[0].count);
|
||||
|
||||
const oldEvents = await client.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM calendar_events
|
||||
WHERE end_time < NOW() - INTERVAL '${config.pastEventsDays} days'
|
||||
`);
|
||||
preview.calendarEvents = parseInt(oldEvents.rows[0].count);
|
||||
|
||||
preview.total = preview.rssFeeds + preview.globalTodos + preview.noteTodos + preview.archivedNotes + preview.calendarEvents;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
config,
|
||||
preview
|
||||
});
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la prévisualisation de la purge:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur lors de la prévisualisation',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/cleanup/status
|
||||
* Obtenir le statut du scheduler de purge
|
||||
*/
|
||||
router.get('/cleanup/status', (req, res) => {
|
||||
try {
|
||||
const status = cleanupScheduler.getStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
status
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération du statut du scheduler:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur lors de la récupération du statut',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/stats
|
||||
* Obtenir les statistiques de la base de données
|
||||
*/
|
||||
router.get('/stats', async (req, res) => {
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
const stats = {};
|
||||
|
||||
try {
|
||||
// Statistiques générales
|
||||
const users = await client.query('SELECT COUNT(*) as count FROM users');
|
||||
stats.users = parseInt(users.rows[0].count);
|
||||
|
||||
const notes = await client.query('SELECT COUNT(*) as count FROM notes');
|
||||
stats.notes = parseInt(notes.rows[0].count);
|
||||
|
||||
const archivedNotes = await client.query('SELECT COUNT(*) as count FROM notes WHERE archived = TRUE');
|
||||
stats.archivedNotes = parseInt(archivedNotes.rows[0].count);
|
||||
|
||||
const globalTodos = await client.query('SELECT COUNT(*) as count FROM global_todos');
|
||||
stats.globalTodos = parseInt(globalTodos.rows[0].count);
|
||||
|
||||
const completedGlobalTodos = await client.query('SELECT COUNT(*) as count FROM global_todos WHERE completed = TRUE');
|
||||
stats.completedGlobalTodos = parseInt(completedGlobalTodos.rows[0].count);
|
||||
|
||||
const noteTodos = await client.query('SELECT COUNT(*) as count FROM note_todos');
|
||||
stats.noteTodos = parseInt(noteTodos.rows[0].count);
|
||||
|
||||
const completedNoteTodos = await client.query('SELECT COUNT(*) as count FROM note_todos WHERE completed = TRUE');
|
||||
stats.completedNoteTodos = parseInt(completedNoteTodos.rows[0].count);
|
||||
|
||||
const rssFeeds = await client.query('SELECT COUNT(*) as count FROM rss_feeds');
|
||||
stats.rssFeeds = parseInt(rssFeeds.rows[0].count);
|
||||
|
||||
const enabledFeeds = await client.query('SELECT COUNT(*) as count FROM rss_feeds WHERE enabled = TRUE');
|
||||
stats.enabledFeeds = parseInt(enabledFeeds.rows[0].count);
|
||||
|
||||
const rssArticles = await client.query('SELECT COUNT(*) as count FROM rss_articles');
|
||||
stats.rssArticles = parseInt(rssArticles.rows[0].count);
|
||||
|
||||
const calendarEvents = await client.query('SELECT COUNT(*) as count FROM calendar_events');
|
||||
stats.calendarEvents = parseInt(calendarEvents.rows[0].count);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
stats
|
||||
});
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Erreur lors de la récupération des statistiques:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur lors de la récupération des statistiques',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user