mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-22 22:58:15 +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
@@ -59,6 +59,7 @@ async function initDatabase() {
|
|||||||
content TEXT,
|
content TEXT,
|
||||||
image_filename TEXT,
|
image_filename TEXT,
|
||||||
archived BOOLEAN DEFAULT FALSE,
|
archived BOOLEAN DEFAULT FALSE,
|
||||||
|
archived_at TIMESTAMP,
|
||||||
priority INTEGER DEFAULT 0,
|
priority INTEGER DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -72,7 +73,9 @@ async function initDatabase() {
|
|||||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
completed BOOLEAN DEFAULT FALSE,
|
completed BOOLEAN DEFAULT FALSE,
|
||||||
position INTEGER DEFAULT 0
|
completed_at TIMESTAMP,
|
||||||
|
position INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -83,6 +86,7 @@ async function initDatabase() {
|
|||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
completed BOOLEAN DEFAULT FALSE,
|
completed BOOLEAN DEFAULT FALSE,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -221,6 +225,78 @@ async function initDatabase() {
|
|||||||
|
|
||||||
logger.info('✓ Tables PostgreSQL créées avec succès');
|
logger.info('✓ Tables PostgreSQL créées avec succès');
|
||||||
|
|
||||||
|
// Créer les triggers pour mise à jour automatique des dates de purge
|
||||||
|
// Trigger pour notes.archived_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_notes_archived_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.archived = TRUE AND OLD.archived = FALSE THEN
|
||||||
|
NEW.archived_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.archived = FALSE THEN
|
||||||
|
NEW.archived_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_notes_archived_at ON notes;
|
||||||
|
CREATE TRIGGER trigger_notes_archived_at
|
||||||
|
BEFORE UPDATE ON notes
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_notes_archived_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Trigger pour global_todos.completed_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_global_todos_completed_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.completed = TRUE AND OLD.completed = FALSE THEN
|
||||||
|
NEW.completed_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.completed = FALSE THEN
|
||||||
|
NEW.completed_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_global_todos_completed_at ON global_todos;
|
||||||
|
CREATE TRIGGER trigger_global_todos_completed_at
|
||||||
|
BEFORE UPDATE ON global_todos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_global_todos_completed_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Trigger pour note_todos.completed_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_note_todos_completed_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.completed = TRUE AND (OLD.completed IS NULL OR OLD.completed = FALSE) THEN
|
||||||
|
NEW.completed_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.completed = FALSE THEN
|
||||||
|
NEW.completed_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_note_todos_completed_at ON note_todos;
|
||||||
|
CREATE TRIGGER trigger_note_todos_completed_at
|
||||||
|
BEFORE UPDATE ON note_todos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_note_todos_completed_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
logger.info('✓ Triggers de purge automatique créés');
|
||||||
|
|
||||||
// Créer l'utilisateur admin par défaut si la table est vide
|
// Créer l'utilisateur admin par défaut si la table est vide
|
||||||
const usersCount = await client.query('SELECT COUNT(*) as count FROM users');
|
const usersCount = await client.query('SELECT COUNT(*) as count FROM users');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
# Purge Automatique de la Base de Données
|
||||||
|
|
||||||
|
Ce document décrit le système de purge automatique de la base de données NoteFlow.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le système de purge automatique supprime périodiquement les données obsolètes pour maintenir la base de données propre et performante. Il s'exécute automatiquement en arrière-plan et peut également être déclenché manuellement.
|
||||||
|
|
||||||
|
## Éléments Purgés
|
||||||
|
|
||||||
|
La purge supprime automatiquement :
|
||||||
|
|
||||||
|
1. **Flux RSS désactivés** - Tous les flux où `enabled = FALSE`
|
||||||
|
2. **Tâches complétées** - Tâches globales et de notes complétées depuis plus de 3 mois (par défaut)
|
||||||
|
3. **Notes archivées** - Notes archivées depuis plus de 6 mois (par défaut)
|
||||||
|
4. **Rendez-vous passés** - Rendez-vous terminés depuis plus de 6 mois (par défaut)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Migration de la Base de Données
|
||||||
|
|
||||||
|
Avant d'utiliser le système de purge, vous devez exécuter la migration pour ajouter les champs de tracking nécessaires :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette migration ajoute :
|
||||||
|
- `archived_at` à la table `notes`
|
||||||
|
- `completed_at` aux tables `global_todos` et `note_todos`
|
||||||
|
- `created_at` à la table `note_todos`
|
||||||
|
- Triggers automatiques pour mettre à jour ces dates
|
||||||
|
|
||||||
|
**Note:** La migration est idempotente et peut être exécutée plusieurs fois sans problème.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Le système de purge se configure via des variables d'environnement dans votre fichier `.env` ou `docker-compose.yml` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Activer/désactiver la purge automatique (défaut: true)
|
||||||
|
CLEANUP_ENABLED=true
|
||||||
|
|
||||||
|
# Intervalle entre les purges en heures (défaut: 24)
|
||||||
|
CLEANUP_INTERVAL_HOURS=24
|
||||||
|
|
||||||
|
# Délai avant suppression des tâches complétées en jours (défaut: 90)
|
||||||
|
CLEANUP_COMPLETED_TASKS_DAYS=90
|
||||||
|
|
||||||
|
# Délai avant suppression des notes archivées en jours (défaut: 180)
|
||||||
|
CLEANUP_ARCHIVED_NOTES_DAYS=180
|
||||||
|
|
||||||
|
# Délai avant suppression des rendez-vous passés en jours (défaut: 180)
|
||||||
|
CLEANUP_PAST_EVENTS_DAYS=180
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemple de configuration Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
notes-app:
|
||||||
|
environment:
|
||||||
|
- CLEANUP_ENABLED=true
|
||||||
|
- CLEANUP_INTERVAL_HOURS=24
|
||||||
|
- CLEANUP_COMPLETED_TASKS_DAYS=90
|
||||||
|
- CLEANUP_ARCHIVED_NOTES_DAYS=180
|
||||||
|
- CLEANUP_PAST_EVENTS_DAYS=180
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Purge Automatique
|
||||||
|
|
||||||
|
Le scheduler de purge démarre automatiquement avec l'application si `CLEANUP_ENABLED=true`.
|
||||||
|
|
||||||
|
- **Première exécution** : 1 minute après le démarrage de l'application
|
||||||
|
- **Exécutions suivantes** : Selon l'intervalle configuré (défaut: toutes les 24h)
|
||||||
|
|
||||||
|
Les logs de purge apparaissent dans les logs de l'application :
|
||||||
|
|
||||||
|
```
|
||||||
|
═══════════════════════════════════════════════════
|
||||||
|
🧹 DÉMARRAGE DU SCHEDULER DE PURGE AUTOMATIQUE
|
||||||
|
═══════════════════════════════════════════════════
|
||||||
|
Configuration:
|
||||||
|
• Intervalle: toutes les 24 heure(s)
|
||||||
|
• Tâches complétées: > 90 jours
|
||||||
|
• Notes archivées: > 180 jours
|
||||||
|
• Rendez-vous passés: > 180 jours
|
||||||
|
• Première exécution: dans 60 secondes
|
||||||
|
═══════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purge Manuelle via Script
|
||||||
|
|
||||||
|
#### Mode Simulation (Dry Run)
|
||||||
|
|
||||||
|
Pour voir ce qui serait supprimé **sans effectuer de suppression** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:cleanup:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Purge Réelle
|
||||||
|
|
||||||
|
Pour exécuter la purge immédiatement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dans Docker :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simulation
|
||||||
|
docker-compose exec notes-app npm run db:cleanup:dry-run
|
||||||
|
|
||||||
|
# Purge réelle
|
||||||
|
docker-compose exec notes-app npm run db:cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purge Manuelle via API
|
||||||
|
|
||||||
|
L'API admin permet de contrôler la purge depuis l'application.
|
||||||
|
|
||||||
|
**Important:** Toutes les routes API nécessitent une authentification admin.
|
||||||
|
|
||||||
|
#### 1. Prévisualiser la Purge
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/admin/cleanup/preview
|
||||||
|
```
|
||||||
|
|
||||||
|
**Paramètres de requête (optionnels) :**
|
||||||
|
- `completedTasksDays` - Délai pour les tâches (défaut: 90)
|
||||||
|
- `archivedNotesDays` - Délai pour les notes (défaut: 180)
|
||||||
|
- `pastEventsDays` - Délai pour les rendez-vous (défaut: 180)
|
||||||
|
|
||||||
|
**Exemple :**
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
"http://localhost:2222/api/admin/cleanup/preview?completedTasksDays=60"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"config": {
|
||||||
|
"completedTasksDays": 60,
|
||||||
|
"archivedNotesDays": 180,
|
||||||
|
"pastEventsDays": 180
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"rssFeeds": 2,
|
||||||
|
"globalTodos": 45,
|
||||||
|
"noteTodos": 123,
|
||||||
|
"archivedNotes": 8,
|
||||||
|
"calendarEvents": 156,
|
||||||
|
"total": 334
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Exécuter la Purge
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/admin/cleanup
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"dryRun": false,
|
||||||
|
"config": {
|
||||||
|
"completedTasksDays": 90,
|
||||||
|
"archivedNotesDays": 180,
|
||||||
|
"pastEventsDays": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Paramètres :**
|
||||||
|
- `dryRun` (boolean, optionnel) - Mode simulation (défaut: false)
|
||||||
|
- `config` (object, optionnel) - Configuration personnalisée
|
||||||
|
|
||||||
|
**Exemple avec curl :**
|
||||||
|
```bash
|
||||||
|
# Simulation
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"dryRun": true}' \
|
||||||
|
http://localhost:2222/api/admin/cleanup
|
||||||
|
|
||||||
|
# Purge réelle
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"dryRun": false}' \
|
||||||
|
http://localhost:2222/api/admin/cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"dryRun": false,
|
||||||
|
"config": {
|
||||||
|
"completedTasksDays": 90,
|
||||||
|
"archivedNotesDays": 180,
|
||||||
|
"pastEventsDays": 180
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"rssFeeds": 2,
|
||||||
|
"globalTodos": 45,
|
||||||
|
"noteTodos": 123,
|
||||||
|
"archivedNotes": 8,
|
||||||
|
"calendarEvents": 156
|
||||||
|
},
|
||||||
|
"total": 334,
|
||||||
|
"message": "Purge terminée avec succès: 334 élément(s) supprimés"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Obtenir le Statut du Scheduler
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/admin/cleanup/status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": {
|
||||||
|
"enabled": true,
|
||||||
|
"running": false,
|
||||||
|
"scheduled": true,
|
||||||
|
"config": {
|
||||||
|
"intervalHours": 24,
|
||||||
|
"completedTasksDays": 90,
|
||||||
|
"archivedNotesDays": 180,
|
||||||
|
"pastEventsDays": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Obtenir les Statistiques de la Base de Données
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/admin/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"stats": {
|
||||||
|
"users": 3,
|
||||||
|
"notes": 245,
|
||||||
|
"archivedNotes": 18,
|
||||||
|
"globalTodos": 67,
|
||||||
|
"completedGlobalTodos": 142,
|
||||||
|
"noteTodos": 456,
|
||||||
|
"completedNoteTodos": 389,
|
||||||
|
"rssFeeds": 12,
|
||||||
|
"enabledFeeds": 10,
|
||||||
|
"rssArticles": 1234,
|
||||||
|
"calendarEvents": 245
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fonctionnement Technique
|
||||||
|
|
||||||
|
### Champs de Tracking
|
||||||
|
|
||||||
|
Le système utilise des champs de date automatiquement mis à jour par des triggers PostgreSQL :
|
||||||
|
|
||||||
|
- **`notes.archived_at`** - Date à laquelle la note a été archivée
|
||||||
|
- **`global_todos.completed_at`** - Date à laquelle la tâche a été complétée
|
||||||
|
- **`note_todos.completed_at`** - Date à laquelle la tâche a été complétée
|
||||||
|
|
||||||
|
### Triggers PostgreSQL
|
||||||
|
|
||||||
|
Des triggers automatiques mettent à jour ces dates :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Exemple pour les notes
|
||||||
|
CREATE TRIGGER trigger_notes_archived_at
|
||||||
|
BEFORE UPDATE ON notes
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_notes_archived_at();
|
||||||
|
```
|
||||||
|
|
||||||
|
Lorsqu'une note passe de `archived = FALSE` à `archived = TRUE`, le trigger définit automatiquement `archived_at` à la date/heure actuelle.
|
||||||
|
|
||||||
|
### Suppressions en CASCADE
|
||||||
|
|
||||||
|
Les suppressions utilisent les contraintes `ON DELETE CASCADE` de PostgreSQL pour supprimer automatiquement les données liées :
|
||||||
|
|
||||||
|
- Supprimer une note supprime automatiquement :
|
||||||
|
- Ses tâches (`note_todos`)
|
||||||
|
- Ses images (`note_images`)
|
||||||
|
- Ses fichiers (`note_files`)
|
||||||
|
- Ses tags (`note_tags`)
|
||||||
|
|
||||||
|
- Supprimer un flux RSS supprime automatiquement :
|
||||||
|
- Ses articles (`rss_articles`)
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
- ✅ Toutes les routes API nécessitent une authentification admin
|
||||||
|
- ✅ Les transactions SQL utilisent des requêtes préparées (protection contre SQL injection)
|
||||||
|
- ✅ Les suppressions utilisent `BEGIN/COMMIT/ROLLBACK` pour garantir l'intégrité
|
||||||
|
- ✅ Mode simulation disponible pour tester sans risque
|
||||||
|
|
||||||
|
## Désactivation
|
||||||
|
|
||||||
|
Pour désactiver complètement la purge automatique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dans .env ou docker-compose.yml
|
||||||
|
CLEANUP_ENABLED=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis redémarrer l'application :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose restart notes-app
|
||||||
|
# ou
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dépannage
|
||||||
|
|
||||||
|
### La purge ne s'exécute pas
|
||||||
|
|
||||||
|
1. Vérifiez que `CLEANUP_ENABLED=true`
|
||||||
|
2. Vérifiez les logs de l'application :
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f notes-app
|
||||||
|
```
|
||||||
|
3. Vérifiez que la migration a été exécutée :
|
||||||
|
```bash
|
||||||
|
npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreur "Column archived_at does not exist"
|
||||||
|
|
||||||
|
La migration n'a pas été exécutée. Lancez :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voir les détails de la purge
|
||||||
|
|
||||||
|
Utilisez le mode simulation :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:cleanup:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou via l'API :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"dryRun": true}' \
|
||||||
|
http://localhost:2222/api/admin/cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommandations
|
||||||
|
|
||||||
|
1. **Testez d'abord en simulation** - Utilisez `dryRun: true` ou `npm run db:cleanup:dry-run`
|
||||||
|
2. **Sauvegardez régulièrement** - Configurez des sauvegardes PostgreSQL automatiques
|
||||||
|
3. **Ajustez les délais** - Adaptez `CLEANUP_*_DAYS` selon vos besoins
|
||||||
|
4. **Surveillez les logs** - Vérifiez régulièrement les logs de purge
|
||||||
|
5. **Commencez conservateur** - Utilisez des délais plus longs au début (ex: 6 mois au lieu de 3)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Pour plus d'informations :
|
||||||
|
- Consultez les logs : `docker-compose logs notes-app`
|
||||||
|
- Vérifiez la configuration : `GET /api/admin/cleanup/status`
|
||||||
|
- Testez en simulation : `npm run db:cleanup:dry-run`
|
||||||
+4
-1
@@ -15,7 +15,10 @@
|
|||||||
"docker:up": "docker-compose up -d",
|
"docker:up": "docker-compose up -d",
|
||||||
"docker:down": "docker-compose down",
|
"docker:down": "docker-compose down",
|
||||||
"docker:logs": "docker-compose logs -f",
|
"docker:logs": "docker-compose logs -f",
|
||||||
"docker:restart": "docker-compose restart"
|
"docker:restart": "docker-compose restart",
|
||||||
|
"db:migrate": "node scripts/add-cleanup-tracking-fields.js",
|
||||||
|
"db:cleanup": "node scripts/database-cleanup.js",
|
||||||
|
"db:cleanup:dry-run": "DRY_RUN=true node scripts/database-cleanup.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"notes",
|
"notes",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de migration pour ajouter les champs de tracking pour la purge automatique
|
||||||
|
* - archived_at pour les notes
|
||||||
|
* - completed_at pour global_todos et note_todos
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 migrate() {
|
||||||
|
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ MIGRATION: Ajout des champs de tracking pour purge ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// 1. Ajouter archived_at à la table notes
|
||||||
|
console.log('📝 Ajout du champ archived_at à la table notes...');
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE notes
|
||||||
|
ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP
|
||||||
|
`);
|
||||||
|
console.log('✅ Champ archived_at ajouté\n');
|
||||||
|
|
||||||
|
// 2. Mettre à jour les notes déjà archivées avec updated_at comme date approximative
|
||||||
|
console.log('🔄 Mise à jour des notes archivées existantes...');
|
||||||
|
const archivedNotesUpdate = await client.query(`
|
||||||
|
UPDATE notes
|
||||||
|
SET archived_at = updated_at
|
||||||
|
WHERE archived = TRUE AND archived_at IS NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ ${archivedNotesUpdate.rowCount} note(s) archivée(s) mise(s) à jour\n`);
|
||||||
|
|
||||||
|
// 3. Ajouter completed_at à la table global_todos
|
||||||
|
console.log('📝 Ajout du champ completed_at à la table global_todos...');
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE global_todos
|
||||||
|
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP
|
||||||
|
`);
|
||||||
|
console.log('✅ Champ completed_at ajouté à global_todos\n');
|
||||||
|
|
||||||
|
// 4. Mettre à jour les global_todos déjà complétées
|
||||||
|
console.log('🔄 Mise à jour des tâches globales complétées existantes...');
|
||||||
|
const globalTodosUpdate = await client.query(`
|
||||||
|
UPDATE global_todos
|
||||||
|
SET completed_at = created_at
|
||||||
|
WHERE completed = TRUE AND completed_at IS NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ ${globalTodosUpdate.rowCount} tâche(s) globale(s) mise(s) à jour\n`);
|
||||||
|
|
||||||
|
// 5. Ajouter created_at et completed_at à la table note_todos
|
||||||
|
console.log('📝 Ajout des champs created_at et completed_at à la table note_todos...');
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE note_todos
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
`);
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE note_todos
|
||||||
|
ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP
|
||||||
|
`);
|
||||||
|
console.log('✅ Champs created_at et completed_at ajoutés à note_todos\n');
|
||||||
|
|
||||||
|
// 6. Mettre à jour les note_todos déjà complétées
|
||||||
|
console.log('🔄 Mise à jour des tâches de notes complétées existantes...');
|
||||||
|
const noteTodosUpdate = await client.query(`
|
||||||
|
UPDATE note_todos
|
||||||
|
SET completed_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE completed = TRUE AND completed_at IS NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ ${noteTodosUpdate.rowCount} tâche(s) de note(s) mise(s) à jour\n`);
|
||||||
|
|
||||||
|
// 7. Créer les triggers pour mettre à jour automatiquement les dates
|
||||||
|
console.log('🔧 Création des triggers pour mise à jour automatique...');
|
||||||
|
|
||||||
|
// Trigger pour notes.archived_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_notes_archived_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.archived = TRUE AND OLD.archived = FALSE THEN
|
||||||
|
NEW.archived_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.archived = FALSE THEN
|
||||||
|
NEW.archived_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_notes_archived_at ON notes;
|
||||||
|
CREATE TRIGGER trigger_notes_archived_at
|
||||||
|
BEFORE UPDATE ON notes
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_notes_archived_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Trigger pour global_todos.completed_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_global_todos_completed_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.completed = TRUE AND OLD.completed = FALSE THEN
|
||||||
|
NEW.completed_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.completed = FALSE THEN
|
||||||
|
NEW.completed_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_global_todos_completed_at ON global_todos;
|
||||||
|
CREATE TRIGGER trigger_global_todos_completed_at
|
||||||
|
BEFORE UPDATE ON global_todos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_global_todos_completed_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Trigger pour note_todos.completed_at
|
||||||
|
await client.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION update_note_todos_completed_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.completed = TRUE AND (OLD.completed IS NULL OR OLD.completed = FALSE) THEN
|
||||||
|
NEW.completed_at = CURRENT_TIMESTAMP;
|
||||||
|
ELSIF NEW.completed = FALSE THEN
|
||||||
|
NEW.completed_at = NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await client.query(`
|
||||||
|
DROP TRIGGER IF EXISTS trigger_note_todos_completed_at ON note_todos;
|
||||||
|
CREATE TRIGGER trigger_note_todos_completed_at
|
||||||
|
BEFORE UPDATE ON note_todos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_note_todos_completed_at();
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ Triggers créés avec succès\n');
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ ✅ MIGRATION TERMINÉE AVEC SUCCÈS ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
console.log('📝 RÉSUMÉ DES CHANGEMENTS:\n');
|
||||||
|
console.log('• Champ archived_at ajouté à la table notes');
|
||||||
|
console.log('• Champ completed_at ajouté aux tables global_todos et note_todos');
|
||||||
|
console.log('• Triggers créés pour mise à jour automatique des dates');
|
||||||
|
console.log('• Données existantes migrées avec succès\n');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.end();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Erreur:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate();
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script de purge automatique de la base de données
|
||||||
|
*
|
||||||
|
* Purge les éléments suivants :
|
||||||
|
* 1. Flux RSS désactivés (enabled = FALSE)
|
||||||
|
* 2. Tâches complétées depuis plus de 3 mois
|
||||||
|
* 3. Notes archivées depuis plus de 6 mois
|
||||||
|
* 4. Rendez-vous terminés depuis plus de 6 mois
|
||||||
|
*/
|
||||||
|
|
||||||
|
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'}`;
|
||||||
|
|
||||||
|
// Paramètres de purge (en jours)
|
||||||
|
const CLEANUP_CONFIG = {
|
||||||
|
COMPLETED_TASKS_DAYS: 90, // 3 mois
|
||||||
|
ARCHIVED_NOTES_DAYS: 180, // 6 mois
|
||||||
|
PAST_EVENTS_DAYS: 180, // 6 mois
|
||||||
|
DRY_RUN: process.env.DRY_RUN === 'true' // Mode simulation
|
||||||
|
};
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||||
|
const stats = {
|
||||||
|
rssFeeds: 0,
|
||||||
|
rssArticles: 0,
|
||||||
|
globalTodos: 0,
|
||||||
|
noteTodos: 0,
|
||||||
|
archivedNotes: 0,
|
||||||
|
calendarEvents: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ PURGE AUTOMATIQUE DE LA BASE DE DONNÉES ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
if (CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
console.log('⚠️ MODE SIMULATION (DRY_RUN) - Aucune suppression réelle\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 Configuration de la purge:');
|
||||||
|
console.log(` • Tâches complétées: > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours`);
|
||||||
|
console.log(` • Notes archivées: > ${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} jours`);
|
||||||
|
console.log(` • Rendez-vous passés: > ${CLEANUP_CONFIG.PAST_EVENTS_DAYS} jours`);
|
||||||
|
console.log(` • Flux RSS désactivés: Tous\n`);
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// 1. PURGE DES FLUX RSS DÉSACTIVÉS
|
||||||
|
console.log('═══════════════════════════════════════════════════════════');
|
||||||
|
console.log('📡 1. FLUX RSS DÉSACTIVÉS');
|
||||||
|
console.log('═══════════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
// Compter les flux désactivés
|
||||||
|
const disabledFeeds = await client.query(`
|
||||||
|
SELECT id, title, url, enabled
|
||||||
|
FROM rss_feeds
|
||||||
|
WHERE enabled = FALSE
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`📊 Flux RSS désactivés trouvés: ${disabledFeeds.rows.length}`);
|
||||||
|
|
||||||
|
if (disabledFeeds.rows.length > 0) {
|
||||||
|
console.log('\n📝 Détails des flux à supprimer:');
|
||||||
|
disabledFeeds.rows.forEach((feed, index) => {
|
||||||
|
console.log(` ${index + 1}. [ID: ${feed.id}] ${feed.title || 'Sans titre'}`);
|
||||||
|
console.log(` URL: ${feed.url}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
// Les articles seront supprimés en CASCADE
|
||||||
|
const deleteFeeds = await client.query(`
|
||||||
|
DELETE FROM rss_feeds
|
||||||
|
WHERE enabled = FALSE
|
||||||
|
`);
|
||||||
|
stats.rssFeeds = deleteFeeds.rowCount;
|
||||||
|
console.log(`\n✅ ${stats.rssFeeds} flux RSS désactivé(s) supprimé(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n🔍 [SIMULATION] ${disabledFeeds.rows.length} flux seraient supprimés`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ Aucun flux RSS désactivé à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. PURGE DES TÂCHES GLOBALES COMPLÉTÉES
|
||||||
|
console.log('\n═══════════════════════════════════════════════════════════');
|
||||||
|
console.log('✅ 2. TÂCHES GLOBALES COMPLÉTÉES');
|
||||||
|
console.log('═══════════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
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 '${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} days'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`📊 Tâches globales complétées > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours: ${oldGlobalTodos.rows[0].count}`);
|
||||||
|
|
||||||
|
if (parseInt(oldGlobalTodos.rows[0].count) > 0) {
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
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;
|
||||||
|
console.log(`✅ ${stats.globalTodos} tâche(s) globale(s) supprimée(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`🔍 [SIMULATION] ${oldGlobalTodos.rows[0].count} tâches seraient supprimées`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ Aucune tâche globale ancienne à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. PURGE DES TÂCHES DE NOTES COMPLÉTÉES
|
||||||
|
console.log('\n═══════════════════════════════════════════════════════════');
|
||||||
|
console.log('📝 3. TÂCHES DE NOTES COMPLÉTÉES');
|
||||||
|
console.log('═══════════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
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 '${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} days'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`📊 Tâches de notes complétées > ${CLEANUP_CONFIG.COMPLETED_TASKS_DAYS} jours: ${oldNoteTodos.rows[0].count}`);
|
||||||
|
|
||||||
|
if (parseInt(oldNoteTodos.rows[0].count) > 0) {
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
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;
|
||||||
|
console.log(`✅ ${stats.noteTodos} tâche(s) de note(s) supprimée(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`🔍 [SIMULATION] ${oldNoteTodos.rows[0].count} tâches seraient supprimées`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ Aucune tâche de note ancienne à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. PURGE DES NOTES ARCHIVÉES
|
||||||
|
console.log('\n═══════════════════════════════════════════════════════════');
|
||||||
|
console.log('🗄️ 4. NOTES ARCHIVÉES');
|
||||||
|
console.log('═══════════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
const oldArchivedNotes = await client.query(`
|
||||||
|
SELECT id, title, archived_at
|
||||||
|
FROM notes
|
||||||
|
WHERE archived = TRUE
|
||||||
|
AND archived_at IS NOT NULL
|
||||||
|
AND archived_at < NOW() - INTERVAL '${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} days'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`📊 Notes archivées > ${CLEANUP_CONFIG.ARCHIVED_NOTES_DAYS} jours: ${oldArchivedNotes.rows.length}`);
|
||||||
|
|
||||||
|
if (oldArchivedNotes.rows.length > 0) {
|
||||||
|
console.log('\n📝 Détails des notes à supprimer (10 premières):');
|
||||||
|
oldArchivedNotes.rows.slice(0, 10).forEach((note, index) => {
|
||||||
|
const daysAgo = Math.floor((Date.now() - new Date(note.archived_at).getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
console.log(` ${index + 1}. [ID: ${note.id}] ${note.title}`);
|
||||||
|
console.log(` Archivée il y a ${daysAgo} jours`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldArchivedNotes.rows.length > 10) {
|
||||||
|
console.log(` ... et ${oldArchivedNotes.rows.length - 10} autre(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
// Les fichiers associés (note_todos, note_images, note_files, note_tags) seront supprimés en CASCADE
|
||||||
|
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;
|
||||||
|
console.log(`\n✅ ${stats.archivedNotes} note(s) archivée(s) supprimée(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n🔍 [SIMULATION] ${oldArchivedNotes.rows.length} notes seraient supprimées`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ Aucune note archivée ancienne à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. PURGE DES RENDEZ-VOUS PASSÉS
|
||||||
|
console.log('\n═══════════════════════════════════════════════════════════');
|
||||||
|
console.log('📅 5. RENDEZ-VOUS PASSÉS');
|
||||||
|
console.log('═══════════════════════════════════════════════════════════\n');
|
||||||
|
|
||||||
|
const oldEvents = await client.query(`
|
||||||
|
SELECT id, title, end_time
|
||||||
|
FROM calendar_events
|
||||||
|
WHERE end_time < NOW() - INTERVAL '${CLEANUP_CONFIG.PAST_EVENTS_DAYS} days'
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`📊 Rendez-vous terminés > ${CLEANUP_CONFIG.PAST_EVENTS_DAYS} jours: ${oldEvents.rows.length}`);
|
||||||
|
|
||||||
|
if (oldEvents.rows.length > 0) {
|
||||||
|
console.log('\n📝 Détails des rendez-vous à supprimer (10 premiers):');
|
||||||
|
oldEvents.rows.slice(0, 10).forEach((event, index) => {
|
||||||
|
const daysAgo = Math.floor((Date.now() - new Date(event.end_time).getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
console.log(` ${index + 1}. [ID: ${event.id}] ${event.title}`);
|
||||||
|
console.log(` Terminé il y a ${daysAgo} jours`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldEvents.rows.length > 10) {
|
||||||
|
console.log(` ... et ${oldEvents.rows.length - 10} autre(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
const deleteEvents = await client.query(`
|
||||||
|
DELETE FROM calendar_events
|
||||||
|
WHERE end_time < NOW() - INTERVAL '${CLEANUP_CONFIG.PAST_EVENTS_DAYS} days'
|
||||||
|
`);
|
||||||
|
stats.calendarEvents = deleteEvents.rowCount;
|
||||||
|
console.log(`\n✅ ${stats.calendarEvents} rendez-vous supprimé(s)`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n🔍 [SIMULATION] ${oldEvents.rows.length} rendez-vous seraient supprimés`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ Aucun rendez-vous ancien à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMIT ou ROLLBACK selon le mode
|
||||||
|
if (!CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} else {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
}
|
||||||
|
|
||||||
|
// RÉSUMÉ FINAL
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ RÉSUMÉ DE LA PURGE ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
const totalDeleted = stats.rssFeeds + stats.globalTodos + stats.noteTodos + stats.archivedNotes + stats.calendarEvents;
|
||||||
|
|
||||||
|
console.log('📊 Éléments supprimés:');
|
||||||
|
console.log(` • Flux RSS désactivés: ${stats.rssFeeds}`);
|
||||||
|
console.log(` • Tâches globales: ${stats.globalTodos}`);
|
||||||
|
console.log(` • Tâches de notes: ${stats.noteTodos}`);
|
||||||
|
console.log(` • Notes archivées: ${stats.archivedNotes}`);
|
||||||
|
console.log(` • Rendez-vous: ${stats.calendarEvents}`);
|
||||||
|
console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
||||||
|
console.log(` 📈 TOTAL: ${totalDeleted} élément(s)\n`);
|
||||||
|
|
||||||
|
if (CLEANUP_CONFIG.DRY_RUN) {
|
||||||
|
console.log('⚠️ MODE SIMULATION - Aucune suppression réelle effectuée');
|
||||||
|
console.log('💡 Pour exécuter réellement: DRY_RUN=false npm run cleanup\n');
|
||||||
|
} else {
|
||||||
|
console.log('✅ Purge terminée avec succès!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.end();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Erreur lors de la purge:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter le cleanup
|
||||||
|
if (require.main === module) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporter pour utilisation comme module
|
||||||
|
module.exports = { cleanup, CLEANUP_CONFIG };
|
||||||
@@ -94,6 +94,7 @@ app.use('/api/rss', require('./routes/rss.routes'));
|
|||||||
app.use('/api/calendar', require('./routes/calendar.routes'));
|
app.use('/api/calendar', require('./routes/calendar.routes'));
|
||||||
app.use('/api/openrouter', require('./routes/openrouter.routes'));
|
app.use('/api/openrouter', require('./routes/openrouter.routes'));
|
||||||
app.use('/api/timezone-logs', require('./routes/timezone-logs.routes'));
|
app.use('/api/timezone-logs', require('./routes/timezone-logs.routes'));
|
||||||
|
app.use('/api/admin', require('./routes/admin.routes'));
|
||||||
|
|
||||||
// Route de recherche
|
// Route de recherche
|
||||||
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
||||||
@@ -138,6 +139,11 @@ async function startServer() {
|
|||||||
rssScheduler.startScheduler();
|
rssScheduler.startScheduler();
|
||||||
logger.info('✓ Scheduler RSS démarré');
|
logger.info('✓ Scheduler RSS démarré');
|
||||||
|
|
||||||
|
// Démarrer le scheduler de purge automatique
|
||||||
|
const cleanupScheduler = require('./services/cleanup-scheduler');
|
||||||
|
cleanupScheduler.startScheduler();
|
||||||
|
logger.info('✓ Scheduler de purge automatique démarré');
|
||||||
|
|
||||||
// Démarrer le serveur
|
// Démarrer le serveur
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
logger.info('═════════════════════════════════════════════');
|
logger.info('═════════════════════════════════════════════');
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
// 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
|
||||||
|
};
|
||||||
Reference in new issue
Block a user