diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd02a05..4eb0664 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,72 @@
# Changelog
+## [Unreleased] - Branch: feature/DeleteUnprovedGroups
+
+### ✨ Automatic Cleanup Feature (November 2025)
+
+#### Backend
+- ✅ **Database Schema**: New `deletion_log` table for audit trail
+ - Columns: group_id, year, image_count, upload_date, deleted_at, deletion_reason, total_file_size
+ - Performance indexes: idx_groups_cleanup, idx_groups_approved, idx_deletion_log_deleted_at
+ - Automatic schema migration on server startup
+
+- ✅ **Services**: New cleanup orchestration layer
+ - `GroupCleanupService.js` - Core cleanup logic with 7-day threshold
+ - `SchedulerService.js` - Cron job scheduler (daily at 10:00 AM Europe/Berlin)
+ - Complete file deletion: originals + preview images
+ - Comprehensive logging with statistics
+
+- ✅ **Repositories**: Extended data access layer
+ - `DeletionLogRepository.js` - CRUD operations for deletion history
+ - `GroupRepository.js` - New methods:
+ - `findUnapprovedGroupsOlderThan()` - Query old unapproved groups
+ - `getGroupStatistics()` - Gather metadata before deletion
+ - `deleteGroupCompletely()` - Transactional deletion with CASCADE
+
+- ✅ **API Endpoints**: Admin API routes (`/api/admin/*`)
+ - `GET /deletion-log?limit=N` - Recent deletions with pagination
+ - `GET /deletion-log/all` - Complete deletion history
+ - `GET /deletion-log/stats` - Statistics with formatted file sizes
+ - `POST /cleanup/trigger` - Manual cleanup trigger (testing)
+ - `GET /cleanup/preview` - Dry-run preview of deletions
+
+- ✅ **Dependencies**: Added `node-cron@3.0.3` for scheduled tasks
+
+#### Frontend
+- ✅ **Components**: New deletion log display
+ - `DeletionLogSection.js` - Statistics cards + history table
+ - Statistics: Total groups/images deleted, storage freed
+ - Table: Group ID, year, image count, timestamps, reason, file size
+ - Toggle: "Last 10" / "All" entries with dynamic loading
+
+- ✅ **Moderation Page**: Integrated cleanup features
+ - **Countdown Widget**: Shows "⏰ X Tage bis Löschung" on pending groups
+ - **Approval Feedback**: SweetAlert2 success/error notifications
+ - **Deletion Log**: Integrated at bottom of moderation interface
+ - Visual indicators for pending vs. approved status
+
+- ✅ **Dependencies**: Added `sweetalert2` for user feedback
+
+#### Infrastructure
+- ✅ **Nginx Configuration**: Updated routes for admin API
+ - Dev + Prod configs updated
+ - `/api/admin` proxy to backend (no separate auth - protected by /moderation access)
+ - Proper request forwarding with headers
+
+#### Testing
+- ✅ **Test Tools**: Comprehensive testing utilities
+ - `tests/test-cleanup.sh` - Interactive bash test script
+ - `backend/src/scripts/test-cleanup.js` - Node.js test alternative
+ - Features: Backdate groups, preview cleanup, trigger manually, view logs
+ - `tests/TESTING-CLEANUP.md` - Complete testing guide with 6 scenarios
+
+#### Documentation
+- ✅ **README.md**: Updated with automatic cleanup features
+- ✅ **TESTING-CLEANUP.md**: Comprehensive testing guide
+- ✅ **Code Comments**: Detailed inline documentation
+
+---
+
## [Unreleased] - Branch: feature/ImageDescription
### ✨ Image Descriptions Feature (November 2025)
diff --git a/README.md b/README.md
index edc6802..386bf98 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,8 @@ A self-hosted image uploader with multi-image upload capabilities and automatic
## Features
**Multi-Image Upload**: Upload multiple images at once with batch processing
+**Automatic Cleanup**: 🆕 Unapproved groups are automatically deleted after 7 days
+**Deletion Log**: 🆕 Complete audit trail of automatically deleted content
**Drag-and-Drop Reordering**: 🆕 Admins can reorder images via intuitive drag-and-drop interface
**Slideshow Mode**: Automatic fullscreen slideshow with smooth transitions (respects custom ordering)
**Preview Image Optimization**: Automatic thumbnail generation for faster gallery loading (96-98% size reduction)
@@ -19,12 +21,17 @@ A self-hosted image uploader with multi-image upload capabilities and automatic
This project extends the original [Image-Uploader by vallezw](https://github.com/vallezw/Image-Uploader) with enhanced multi-upload and slideshow capabilities.
### 🆕 Latest Features (November 2025)
-- **Image Descriptions**: 🆕 Add optional descriptions to individual images (max 200 characters)
+- **Automatic Cleanup**: Unapproved groups are automatically deleted after 7 days
+- **Deletion Log**: Complete audit trail with statistics (groups, images, storage freed)
+- **Countdown Display**: Visual indicator showing days until automatic deletion
+- **Approval Feedback**: SweetAlert2 notifications for moderation actions
+- **Manual Cleanup Trigger**: Admin API endpoints for testing and manual cleanup
+- **Image Descriptions**: Add optional descriptions to individual images (max 200 characters)
- **Edit Mode**: Edit descriptions for uploaded images in upload preview and moderation interface
- **Slideshow Display**: Image descriptions shown as overlays during slideshow presentation
- **Public Display**: Descriptions visible in public group views and galleries
-### Previous Features (January 2025)
+### Previous Features (October 2025)
- **Drag-and-Drop Image Reordering**: Admins can now reorder images using intuitive drag-and-drop
- **Touch-Friendly Interface**: Mobile-optimized controls with always-visible drag handles
- **Slideshow Integration**: Custom image order automatically applies to slideshow mode
@@ -128,11 +135,23 @@ The application automatically generates optimized preview thumbnails for all upl
- **Authentication**: HTTP Basic Auth (username: admin, password: set during setup)
- **Features**:
- Review pending image groups before public display
- - Approve or reject submitted collections
+ - Visual countdown showing days until automatic deletion (7 days for unapproved groups)
+ - Approve or reject submitted collections with instant feedback
- Delete individual images from approved groups
- View group details (title, creator, description, image count)
+ - **Deletion Log** (bottom of moderation page):
+ - Statistics: Total groups/images deleted, storage freed
+ - Detailed history table with timestamps and reasons
+ - Toggle between last 10 entries and complete history
- Bulk moderation actions
+- **Automatic Cleanup**:
+ - Unapproved groups are automatically deleted after 7 days
+ - Daily cleanup runs at 10:00 AM (Europe/Berlin timezone)
+ - Complete removal: Database entries + physical files (originals + previews)
+ - Full audit trail logged for compliance
+ - **Note**: Approved groups are NEVER automatically deleted
+
- **Security Features**:
- Password protected access via nginx HTTP Basic Auth
- Hidden from search engines (`robots.txt` + `noindex` meta tags)
@@ -284,15 +303,49 @@ src
### Moderation Operations (Protected)
- `GET /moderation/groups` - Get all groups pending moderation
-- `POST /groups/:id/approve` - Approve a group for public display
+- `PATCH /groups/:id/approve` - Approve/unapprove a group for public display
- `DELETE /groups/:id` - Delete an entire group
- `DELETE /groups/:id/images/:imageId` - Delete individual image from group
+### Admin Operations (Protected by /moderation access)
+
+- `GET /api/admin/deletion-log?limit=N` - Get recent deletion log entries (default: 10)
+- `GET /api/admin/deletion-log/all` - Get complete deletion history
+- `GET /api/admin/deletion-log/stats` - Get deletion statistics (total groups/images deleted, storage freed)
+- `POST /api/admin/cleanup/trigger` - Manually trigger cleanup (for testing)
+- `GET /api/admin/cleanup/preview` - Preview which groups would be deleted (dry-run)
+
### File Access
- `GET /api/upload/:filename` - Access uploaded image files (legacy, use `/api/download` instead)
- `GET /api/download/:filename` - Download original full-resolution images
- `GET /api/previews/:filename` - Access optimized preview thumbnails (~100KB, 800px width)
+## Testing
+
+### Automatic Cleanup Testing
+
+The application includes comprehensive testing tools for the automatic cleanup feature:
+
+```bash
+# Run interactive test helper (recommended)
+./tests/test-cleanup.sh
+
+# Available test operations:
+# 1. View unapproved groups with age
+# 2. Backdate groups for testing (simulate 7+ day old groups)
+# 3. Preview cleanup (dry-run)
+# 4. Execute cleanup manually
+# 5. View deletion log history
+```
+
+**Testing Workflow:**
+1. Upload a test group (don't approve it)
+2. Use test script to backdate it by 8 days
+3. Preview what would be deleted
+4. Execute cleanup and verify deletion log
+
+For detailed testing instructions, see: [`tests/TESTING-CLEANUP.md`](tests/TESTING-CLEANUP.md)
+
## Configuration
### Environment Variables
diff --git a/TODO.md b/TODO.md
index cb11347..d0be951 100644
--- a/TODO.md
+++ b/TODO.md
@@ -44,7 +44,20 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
## Backend
[x] Erweiterung der API um die Funktion bestehende Daten zu editieren/aktualisieren
[x] Preview Generierung für hochgeladene Bilder
-[ ] Automatisches Löschen von Groupen, welche nach einer bestimmten Zeit (z.B. 5 Tage) nicht freigegeben wurden
+[x] **Automatisches Löschen nicht freigegebener Gruppen** ✅ ABGESCHLOSSEN
+ - **Status**: Fertiggestellt und getestet
+ - **Feature Plan**: `docs/FEATURE_PLAN-delete-unproved-groups.md`
+ - **Branch**: `feature/DeleteUnprovedGroups`
+ - **Details**:
+ - Automatische Löschung nach 7 Tagen
+ - Countdown-Anzeige in Moderationsansicht
+ - Vollständiges Deletion-Log mit Statistiken
+ - Täglicher Cron-Job (10:00 Uhr)
+ - Test-Tools: `tests/test-cleanup.sh` und `tests/TESTING-CLEANUP.md`
+ - **Aufgaben**: 11 Tasks (DB Migration + Backend Cleanup Service + Cron-Job + Frontend UI)
+ - **Geschätzte Zeit**: 2-3 Tage
+ - **Löschfrist**: 7 Tage nach Upload (nur nicht freigegebene Gruppen)
+ - **Cron-Job**: Täglich 10:00 Uhr
[ ] Integration eines Benachrichtigungssystems (E-Mail, Push-Benachrichtigungen) wenn eine neue Slideshow hochgeladen wurde
[ ] Implementierung eines Logging-Systems zur Nachverfolgung von Änderungen und Aktivitäten
@@ -52,7 +65,7 @@ Neue Struktur: Datenbank in src/data/db und bilder in src/data/images
[x] Erweiterung der Benutzeroberfläche um eine Editierfunktion für bestehende Einträge in ModerationPage.js
[x] In der angezeigten Gruppen soll statt Bilder ansehen Gruppe editieren stehen
[x] Diese bestehende Ansicht (Bilder ansehen) soll als eigene Seite implementiert werden
- [ ] Ergänzung der Möglichkeit eine Beschreibung zu den Bildern hinzuzufügen
+ [x] Ergänzung der Möglichkeit eine Beschreibung zu den Bildern hinzuzufügen
[ ] Erweiterung der ModerationPage um reine Datenbankeditor der sqlite Datenbank.
diff --git a/backend/package.json b/backend/package.json
index a320402..6272ac3 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -20,6 +20,7 @@
"express-fileupload": "^1.2.1",
"find-remove": "^2.0.3",
"fs": "^0.0.1-security",
+ "node-cron": "^4.2.1",
"sharp": "^0.34.4",
"shortid": "^2.2.16",
"sqlite3": "^5.1.7"
diff --git a/backend/src/database/DatabaseManager.js b/backend/src/database/DatabaseManager.js
index 861f4c0..33dd283 100644
--- a/backend/src/database/DatabaseManager.js
+++ b/backend/src/database/DatabaseManager.js
@@ -115,12 +115,31 @@ class DatabaseManager {
}
}
+ // Erstelle Deletion Log Tabelle
+ await this.run(`
+ CREATE TABLE IF NOT EXISTS deletion_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ group_id TEXT NOT NULL,
+ year INTEGER NOT NULL,
+ image_count INTEGER NOT NULL,
+ upload_date DATETIME NOT NULL,
+ deleted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ deletion_reason TEXT DEFAULT 'auto_cleanup_7days',
+ total_file_size INTEGER
+ )
+ `);
+ console.log('✓ Deletion Log Tabelle erstellt');
+
// Erstelle Indizes
await this.run('CREATE INDEX IF NOT EXISTS idx_groups_group_id ON groups(group_id)');
await this.run('CREATE INDEX IF NOT EXISTS idx_groups_year ON groups(year)');
await this.run('CREATE INDEX IF NOT EXISTS idx_groups_upload_date ON groups(upload_date)');
+ await this.run('CREATE INDEX IF NOT EXISTS idx_groups_approved ON groups(approved)');
+ await this.run('CREATE INDEX IF NOT EXISTS idx_groups_cleanup ON groups(approved, upload_date)');
await this.run('CREATE INDEX IF NOT EXISTS idx_images_group_id ON images(group_id)');
await this.run('CREATE INDEX IF NOT EXISTS idx_images_upload_order ON images(upload_order)');
+ await this.run('CREATE INDEX IF NOT EXISTS idx_deletion_log_deleted_at ON deletion_log(deleted_at DESC)');
+ await this.run('CREATE INDEX IF NOT EXISTS idx_deletion_log_year ON deletion_log(year)');
console.log('✓ Indizes erstellt');
// Erstelle Trigger
diff --git a/backend/src/repositories/DeletionLogRepository.js b/backend/src/repositories/DeletionLogRepository.js
new file mode 100644
index 0000000..9a3aa48
--- /dev/null
+++ b/backend/src/repositories/DeletionLogRepository.js
@@ -0,0 +1,63 @@
+const dbManager = require('../database/DatabaseManager');
+
+class DeletionLogRepository {
+
+ // Erstellt Lösch-Protokoll
+ async createDeletionEntry(logData) {
+ const result = await dbManager.run(`
+ INSERT INTO deletion_log (group_id, year, image_count, upload_date, deletion_reason, total_file_size)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `, [
+ logData.groupId,
+ logData.year,
+ logData.imageCount,
+ logData.uploadDate,
+ logData.deletionReason || 'auto_cleanup_7days',
+ logData.totalFileSize || null
+ ]);
+
+ return result.id;
+ }
+
+ // Hole letzte N Einträge
+ async getRecentDeletions(limit = 10) {
+ const deletions = await dbManager.all(`
+ SELECT * FROM deletion_log
+ ORDER BY deleted_at DESC
+ LIMIT ?
+ `, [limit]);
+
+ return deletions;
+ }
+
+ // Hole alle Einträge (für Admin-Übersicht)
+ async getAllDeletions() {
+ const deletions = await dbManager.all(`
+ SELECT * FROM deletion_log
+ ORDER BY deleted_at DESC
+ `);
+
+ return deletions;
+ }
+
+ // Statistiken (Anzahl gelöschte Gruppen, Bilder, Speicherplatz)
+ async getDeletionStatistics() {
+ const stats = await dbManager.get(`
+ SELECT
+ COUNT(*) as totalDeleted,
+ SUM(image_count) as totalImages,
+ SUM(total_file_size) as totalSize,
+ MAX(deleted_at) as lastCleanup
+ FROM deletion_log
+ `);
+
+ return {
+ totalDeleted: stats.totalDeleted || 0,
+ totalImages: stats.totalImages || 0,
+ totalSize: stats.totalSize || 0,
+ lastCleanup: stats.lastCleanup || null
+ };
+ }
+}
+
+module.exports = new DeletionLogRepository();
diff --git a/backend/src/repositories/GroupRepository.js b/backend/src/repositories/GroupRepository.js
index 34b9475..bfecd5b 100644
--- a/backend/src/repositories/GroupRepository.js
+++ b/backend/src/repositories/GroupRepository.js
@@ -437,6 +437,74 @@ class GroupRepository {
};
});
}
+
+ // Findet Gruppen, die zum Löschen anstehen (approved=false & älter als N Tage)
+ async findUnapprovedGroupsOlderThan(days) {
+ const cutoffDate = new Date();
+ cutoffDate.setDate(cutoffDate.getDate() - days);
+ const cutoffDateStr = cutoffDate.toISOString();
+
+ const groups = await dbManager.all(`
+ SELECT * FROM groups
+ WHERE approved = FALSE
+ AND upload_date < ?
+ ORDER BY upload_date ASC
+ `, [cutoffDateStr]);
+
+ return groups;
+ }
+
+ // Hole Statistiken für Gruppe (für Deletion Log)
+ async getGroupStatistics(groupId) {
+ const group = await dbManager.get(`
+ SELECT * FROM groups WHERE group_id = ?
+ `, [groupId]);
+
+ if (!group) {
+ return null;
+ }
+
+ const images = await dbManager.all(`
+ SELECT file_size, file_path, preview_path FROM images
+ WHERE group_id = ?
+ `, [groupId]);
+
+ const totalFileSize = images.reduce((sum, img) => sum + (img.file_size || 0), 0);
+
+ return {
+ groupId: group.group_id,
+ year: group.year,
+ imageCount: images.length,
+ uploadDate: group.upload_date,
+ totalFileSize: totalFileSize,
+ images: images
+ };
+ }
+
+ // Löscht Gruppe komplett (inkl. DB-Einträge und Dateien)
+ async deleteGroupCompletely(groupId) {
+ return await dbManager.transaction(async (db) => {
+ // Hole alle Bilder der Gruppe (für Datei-Löschung)
+ const images = await db.all(`
+ SELECT file_path, preview_path FROM images
+ WHERE group_id = ?
+ `, [groupId]);
+
+ // Lösche Gruppe (CASCADE löscht automatisch Bilder aus DB)
+ const result = await db.run(`
+ DELETE FROM groups WHERE group_id = ?
+ `, [groupId]);
+
+ if (result.changes === 0) {
+ throw new Error(`Group with ID ${groupId} not found`);
+ }
+
+ return {
+ deletedImages: images.length,
+ imagePaths: images
+ };
+ });
+ }
}
module.exports = new GroupRepository();
\ No newline at end of file
diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
new file mode 100644
index 0000000..38ea942
--- /dev/null
+++ b/backend/src/routes/admin.js
@@ -0,0 +1,138 @@
+const express = require('express');
+const router = express.Router();
+const DeletionLogRepository = require('../repositories/DeletionLogRepository');
+const GroupCleanupService = require('../services/GroupCleanupService');
+
+// GroupCleanupService ist bereits eine Instanz, keine Klasse
+const cleanupService = GroupCleanupService;
+
+// Hole Deletion Log (mit Limit)
+router.get('/deletion-log', async (req, res) => {
+ try {
+ const limit = parseInt(req.query.limit) || 10;
+
+ if (limit < 1 || limit > 1000) {
+ return res.status(400).json({
+ error: 'Invalid limit',
+ message: 'Limit must be between 1 and 1000'
+ });
+ }
+
+ const deletions = await DeletionLogRepository.getRecentDeletions(limit);
+ const total = deletions.length;
+
+ res.json({
+ success: true,
+ deletions: deletions,
+ total: total,
+ limit: limit
+ });
+ } catch (error) {
+ console.error('Error fetching deletion log:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// Hole alle Deletion Logs
+router.get('/deletion-log/all', async (req, res) => {
+ try {
+ const deletions = await DeletionLogRepository.getAllDeletions();
+
+ res.json({
+ success: true,
+ deletions: deletions,
+ total: deletions.length
+ });
+ } catch (error) {
+ console.error('Error fetching all deletion logs:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// Hole Deletion Statistiken
+router.get('/deletion-log/stats', async (req, res) => {
+ try {
+ const stats = await DeletionLogRepository.getDeletionStatistics();
+
+ // Format file size
+ const formatBytes = (bytes) => {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ res.json({
+ success: true,
+ totalDeleted: stats.totalDeleted,
+ totalImages: stats.totalImages,
+ totalSize: formatBytes(stats.totalSize),
+ totalSizeBytes: stats.totalSize,
+ lastCleanup: stats.lastCleanup
+ });
+ } catch (error) {
+ console.error('Error fetching deletion statistics:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// Manueller Cleanup-Trigger (für Testing)
+router.post('/cleanup/trigger', async (req, res) => {
+ try {
+ console.log('[Admin API] Manual cleanup triggered');
+ const result = await cleanupService.performScheduledCleanup();
+
+ res.json({
+ success: true,
+ result: result,
+ message: `Cleanup completed: ${result.deletedGroups} groups deleted`
+ });
+ } catch (error) {
+ console.error('[Admin API] Error triggering cleanup:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+// Zeige welche Gruppen gelöscht würden (Dry-Run)
+router.get('/cleanup/preview', async (req, res) => {
+ try {
+ const groups = await cleanupService.findGroupsForDeletion();
+
+ // Berechne Tage bis zur Löschung für jede Gruppe
+ const groupsWithDays = groups.map(group => ({
+ ...group,
+ daysUntilDeletion: cleanupService.getDaysUntilDeletion(group.uploadDate)
+ }));
+
+ res.json({
+ success: true,
+ groupsToDelete: groupsWithDays.length,
+ groups: groupsWithDays,
+ message: groupsWithDays.length === 0
+ ? 'No groups would be deleted'
+ : `${groupsWithDays.length} groups would be deleted`
+ });
+ } catch (error) {
+ console.error('[Admin API] Error previewing cleanup:', error);
+ res.status(500).json({
+ error: 'Internal server error',
+ message: error.message
+ });
+ }
+});
+
+
+module.exports = router;
diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js
index 932dc3f..5ac95b9 100644
--- a/backend/src/routes/index.js
+++ b/backend/src/routes/index.js
@@ -4,10 +4,12 @@ const batchUploadRouter = require('./batchUpload');
const groupsRouter = require('./groups');
const migrationRouter = require('./migration');
const reorderRouter = require('./reorder');
+const adminRouter = require('./admin');
const renderRoutes = (app) => {
[uploadRouter, downloadRouter, batchUploadRouter, groupsRouter, migrationRouter].forEach(router => app.use('/', router));
app.use('/groups', reorderRouter);
+ app.use('/api/admin', adminRouter);
};
module.exports = { renderRoutes };
\ No newline at end of file
diff --git a/backend/src/scripts/test-cleanup.js b/backend/src/scripts/test-cleanup.js
new file mode 100755
index 0000000..905fa0b
--- /dev/null
+++ b/backend/src/scripts/test-cleanup.js
@@ -0,0 +1,255 @@
+#!/usr/bin/env node
+
+/**
+ * Test-Script für automatisches Löschen
+ *
+ * Dieses Script hilft beim Testen des Cleanup-Features:
+ * 1. Zeigt alle nicht-freigegebenen Gruppen
+ * 2. Erlaubt das Zurückdatieren von Gruppen (für Tests)
+ * 3. Zeigt Preview der zu löschenden Gruppen
+ * 4. Triggert manuellen Cleanup
+ */
+
+const readline = require('readline');
+const https = require('http');
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+const API_BASE = 'http://localhost:5001';
+
+// Helper: HTTP Request
+function makeRequest(path, method = 'GET', data = null) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(path, API_BASE);
+ const options = {
+ hostname: url.hostname,
+ port: url.port,
+ path: url.pathname + url.search,
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ const req = https.request(options, (res) => {
+ let body = '';
+ res.on('data', chunk => body += chunk);
+ res.on('end', () => {
+ try {
+ resolve(JSON.parse(body));
+ } catch (e) {
+ resolve(body);
+ }
+ });
+ });
+
+ req.on('error', reject);
+
+ if (data) {
+ req.write(JSON.stringify(data));
+ }
+
+ req.end();
+ });
+}
+
+// Helper: SQL Query über API
+async function execSQL(query) {
+ // Direkt über docker exec
+ const { exec } = require('child_process');
+ return new Promise((resolve, reject) => {
+ exec(
+ `docker compose -f docker/dev/docker-compose.yml exec -T backend-dev sqlite3 /usr/src/app/src/data/db/image_uploader.db "${query}"`,
+ (error, stdout, stderr) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve(stdout);
+ }
+ );
+ });
+}
+
+// Zeige Menü
+function showMenu() {
+ console.log('\n========================================');
+ console.log(' CLEANUP TEST MENÜ');
+ console.log('========================================');
+ console.log('1. Zeige alle nicht-freigegebenen Gruppen');
+ console.log('2. Gruppe um X Tage zurückdatieren (für Tests)');
+ console.log('3. Preview: Welche Gruppen würden gelöscht?');
+ console.log('4. Cleanup JETZT ausführen');
+ console.log('5. Lösch-Historie anzeigen');
+ console.log('0. Beenden');
+ console.log('========================================\n');
+}
+
+// Option 1: Zeige nicht-freigegebene Gruppen
+async function showUnapprovedGroups() {
+ console.log('\n📋 Lade nicht-freigegebene Gruppen...\n');
+ const result = await execSQL(
+ 'SELECT group_id, year, name, approved, datetime(upload_date) as upload_date, ' +
+ 'CAST((julianday(\\'now\\') - julianday(upload_date)) AS INTEGER) as days_old ' +
+ 'FROM groups WHERE approved = 0 ORDER BY upload_date DESC;'
+ );
+
+ console.log('Gruppe ID | Jahr | Name | Freigegeben | Upload-Datum | Tage alt');
+ console.log('------------- | ---- | --------- | ----------- | -------------------- | --------');
+ console.log(result || 'Keine nicht-freigegebenen Gruppen gefunden.');
+}
+
+// Option 2: Gruppe zurückdatieren
+async function backdateGroup() {
+ await showUnapprovedGroups();
+
+ rl.question('\nGruppe ID zum Zurückdatieren: ', async (groupId) => {
+ if (!groupId) {
+ console.log('❌ Keine Gruppe ID angegeben');
+ return mainMenu();
+ }
+
+ rl.question('Um wie viele Tage zurückdatieren? (z.B. 8 für 8 Tage alt): ', async (days) => {
+ const daysNum = parseInt(days);
+ if (isNaN(daysNum) || daysNum < 1) {
+ console.log('❌ Ungültige Anzahl Tage');
+ return mainMenu();
+ }
+
+ try {
+ await execSQL(
+ `UPDATE groups SET upload_date = datetime('now', '-${daysNum} days') WHERE group_id = '${groupId}';`
+ );
+ console.log(`✅ Gruppe ${groupId} wurde um ${daysNum} Tage zurückdatiert`);
+
+ // Zeige aktualisierte Info
+ const result = await execSQL(
+ `SELECT group_id, datetime(upload_date) as upload_date, ` +
+ `CAST((julianday('now') - julianday(upload_date)) AS INTEGER) as days_old ` +
+ `FROM groups WHERE group_id = '${groupId}';`
+ );
+ console.log('\nAktualisierte Daten:');
+ console.log(result);
+ } catch (error) {
+ console.error('❌ Fehler:', error.message);
+ }
+
+ mainMenu();
+ });
+ });
+}
+
+// Option 3: Preview Cleanup
+async function previewCleanup() {
+ console.log('\n🔍 Lade Cleanup Preview...\n');
+ try {
+ const result = await makeRequest('/api/admin/cleanup/preview');
+
+ if (result.groupsToDelete === 0) {
+ console.log('✅ Keine Gruppen würden gelöscht (alle sind < 7 Tage alt oder freigegeben)');
+ } else {
+ console.log(`⚠️ ${result.groupsToDelete} Gruppe(n) würden gelöscht:\n`);
+ result.groups.forEach(group => {
+ console.log(` - ${group.group_id} (${group.year}) - ${group.name}`);
+ console.log(` Upload: ${group.uploadDate}`);
+ console.log(` Tage seit Upload: ${Math.abs(group.daysUntilDeletion)}`);
+ console.log('');
+ });
+ }
+ } catch (error) {
+ console.error('❌ Fehler:', error.message);
+ }
+
+ mainMenu();
+}
+
+// Option 4: Cleanup ausführen
+async function executeCleanup() {
+ console.log('\n⚠️ ACHTUNG: Dies wird Gruppen permanent löschen!\n');
+
+ rl.question('Cleanup wirklich ausführen? (ja/nein): ', async (answer) => {
+ if (answer.toLowerCase() !== 'ja') {
+ console.log('❌ Abgebrochen');
+ return mainMenu();
+ }
+
+ console.log('\n🔄 Führe Cleanup aus...\n');
+ try {
+ const result = await makeRequest('/api/admin/cleanup/trigger', 'POST');
+
+ console.log('✅ Cleanup abgeschlossen!');
+ console.log(` Gelöschte Gruppen: ${result.result.deletedGroups}`);
+ console.log(` Fehler: ${result.result.failedGroups || 0}`);
+ } catch (error) {
+ console.error('❌ Fehler:', error.message);
+ }
+
+ mainMenu();
+ });
+}
+
+// Option 5: Lösch-Historie
+async function showDeletionLog() {
+ console.log('\n📜 Lösch-Historie (letzte 10 Einträge)...\n');
+ try {
+ const result = await makeRequest('/api/admin/deletion-log?limit=10');
+
+ if (result.deletions.length === 0) {
+ console.log('Keine Einträge im Lösch-Log');
+ } else {
+ console.log('Gruppe ID | Jahr | Bilder | Upload-Datum | Gelöscht am | Grund');
+ console.log('------------- | ---- | ------ | -------------------- | -------------------- | -----');
+ result.deletions.forEach(d => {
+ console.log(
+ `${d.group_id.padEnd(13)} | ${String(d.year).padEnd(4)} | ${String(d.image_count).padEnd(6)} | ` +
+ `${d.upload_date.substring(0, 19)} | ${d.deleted_at.substring(0, 19)} | ${d.deletion_reason}`
+ );
+ });
+ }
+ } catch (error) {
+ console.error('❌ Fehler:', error.message);
+ }
+
+ mainMenu();
+}
+
+// Hauptmenü
+function mainMenu() {
+ showMenu();
+ rl.question('Wähle eine Option: ', async (choice) => {
+ switch (choice) {
+ case '1':
+ await showUnapprovedGroups();
+ mainMenu();
+ break;
+ case '2':
+ await backdateGroup();
+ break;
+ case '3':
+ await previewCleanup();
+ break;
+ case '4':
+ await executeCleanup();
+ break;
+ case '5':
+ await showDeletionLog();
+ break;
+ case '0':
+ console.log('\n👋 Auf Wiedersehen!\n');
+ rl.close();
+ process.exit(0);
+ break;
+ default:
+ console.log('❌ Ungültige Option');
+ mainMenu();
+ }
+ });
+}
+
+// Start
+console.log('\n🚀 Cleanup Test Script gestartet\n');
+console.log('Hinweis: Stelle sicher, dass der Dev-Server läuft (./dev.sh)');
+mainMenu();
diff --git a/backend/src/server.js b/backend/src/server.js
index 5e9176b..48c3857 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -1,6 +1,7 @@
const express = require('express');
const initiateResources = require('./utils/initiate-resources');
const dbManager = require('./database/DatabaseManager');
+const SchedulerService = require('./services/SchedulerService');
class Server {
_port;
@@ -24,6 +25,9 @@ class Server {
this._app.listen(this._port, () => {
console.log(`✅ Server läuft auf Port ${this._port}`);
console.log(`📊 SQLite Datenbank aktiv`);
+
+ // Starte Scheduler für automatisches Cleanup
+ SchedulerService.start();
});
} catch (error) {
console.error('💥 Fehler beim Serverstart:', error);
diff --git a/backend/src/services/GroupCleanupService.js b/backend/src/services/GroupCleanupService.js
new file mode 100644
index 0000000..1af521e
--- /dev/null
+++ b/backend/src/services/GroupCleanupService.js
@@ -0,0 +1,190 @@
+const GroupRepository = require('../repositories/GroupRepository');
+const DeletionLogRepository = require('../repositories/DeletionLogRepository');
+const fs = require('fs').promises;
+const path = require('path');
+
+class GroupCleanupService {
+ constructor() {
+ this.CLEANUP_DAYS = 7; // Gruppen älter als 7 Tage werden gelöscht
+ }
+
+ // Findet alle Gruppen, die gelöscht werden müssen
+ async findGroupsForDeletion() {
+ try {
+ const groups = await GroupRepository.findUnapprovedGroupsOlderThan(this.CLEANUP_DAYS);
+ console.log(`[Cleanup] Found ${groups.length} groups for deletion (older than ${this.CLEANUP_DAYS} days)`);
+ return groups;
+ } catch (error) {
+ console.error('[Cleanup] Error finding groups for deletion:', error);
+ throw error;
+ }
+ }
+
+ // Löscht eine Gruppe vollständig (DB + Dateien)
+ async deleteGroupCompletely(groupId) {
+ try {
+ console.log(`[Cleanup] Starting deletion of group: ${groupId}`);
+
+ // Hole Statistiken vor Löschung
+ const stats = await GroupRepository.getGroupStatistics(groupId);
+ if (!stats) {
+ console.warn(`[Cleanup] Group ${groupId} not found, skipping`);
+ return null;
+ }
+
+ // Lösche Gruppe aus DB (CASCADE löscht Bilder automatisch)
+ const deleteResult = await GroupRepository.deleteGroupCompletely(groupId);
+
+ // Lösche physische Dateien
+ const deletedFiles = await this.deletePhysicalFiles(deleteResult.imagePaths);
+
+ console.log(`[Cleanup] Deleted group ${groupId}: ${deletedFiles.success} files deleted, ${deletedFiles.failed} failed`);
+
+ // Erstelle Deletion Log
+ await this.logDeletion({
+ ...stats,
+ deletedFiles: deletedFiles
+ });
+
+ return {
+ groupId: groupId,
+ imagesDeleted: deleteResult.deletedImages,
+ filesDeleted: deletedFiles.success
+ };
+ } catch (error) {
+ console.error(`[Cleanup] Error deleting group ${groupId}:`, error);
+ throw error;
+ }
+ }
+
+ // Löscht physische Dateien (Bilder + Previews)
+ async deletePhysicalFiles(imagePaths) {
+ const dataDir = path.join(__dirname, '../data');
+ let successCount = 0;
+ let failedCount = 0;
+
+ for (const image of imagePaths) {
+ // Lösche Original-Bild
+ if (image.file_path) {
+ const fullPath = path.join(dataDir, image.file_path);
+ try {
+ await fs.unlink(fullPath);
+ successCount++;
+ } catch (error) {
+ if (error.code !== 'ENOENT') { // Ignoriere "Datei nicht gefunden"
+ console.warn(`[Cleanup] Failed to delete file: ${fullPath}`, error.message);
+ failedCount++;
+ }
+ }
+ }
+
+ // Lösche Preview-Bild
+ if (image.preview_path) {
+ const previewPath = path.join(dataDir, image.preview_path);
+ try {
+ await fs.unlink(previewPath);
+ successCount++;
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ console.warn(`[Cleanup] Failed to delete preview: ${previewPath}`, error.message);
+ failedCount++;
+ }
+ }
+ }
+ }
+
+ return {
+ success: successCount,
+ failed: failedCount
+ };
+ }
+
+ // Erstellt Eintrag im Deletion Log
+ async logDeletion(groupData) {
+ try {
+ await DeletionLogRepository.createDeletionEntry({
+ groupId: groupData.groupId,
+ year: groupData.year,
+ imageCount: groupData.imageCount,
+ uploadDate: groupData.uploadDate,
+ deletionReason: 'auto_cleanup_7days',
+ totalFileSize: groupData.totalFileSize
+ });
+ console.log(`[Cleanup] Logged deletion of group ${groupData.groupId}`);
+ } catch (error) {
+ console.error('[Cleanup] Error logging deletion:', error);
+ // Nicht werfen - Deletion Log ist nicht kritisch
+ }
+ }
+
+ // Hauptmethode: Führt kompletten Cleanup durch
+ async performScheduledCleanup() {
+ const startTime = Date.now();
+ console.log('');
+ console.log('========================================');
+ console.log('[Cleanup] Starting scheduled cleanup...');
+ console.log(`[Cleanup] Date: ${new Date().toISOString()}`);
+ console.log('========================================');
+
+ try {
+ const groupsToDelete = await this.findGroupsForDeletion();
+
+ if (groupsToDelete.length === 0) {
+ console.log('[Cleanup] No groups to delete. Cleanup complete.');
+ console.log('========================================');
+ return {
+ success: true,
+ deletedGroups: 0,
+ message: 'No groups to delete'
+ };
+ }
+
+ let successCount = 0;
+ let failedCount = 0;
+
+ for (const group of groupsToDelete) {
+ try {
+ await this.deleteGroupCompletely(group.group_id);
+ successCount++;
+ } catch (error) {
+ console.error(`[Cleanup] Failed to delete group ${group.group_id}:`, error);
+ failedCount++;
+ }
+ }
+
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
+ console.log('');
+ console.log(`[Cleanup] Cleanup complete!`);
+ console.log(`[Cleanup] Deleted: ${successCount} groups`);
+ console.log(`[Cleanup] Failed: ${failedCount} groups`);
+ console.log(`[Cleanup] Duration: ${duration}s`);
+ console.log('========================================');
+
+ return {
+ success: true,
+ deletedGroups: successCount,
+ failedGroups: failedCount,
+ duration: duration
+ };
+ } catch (error) {
+ console.error('[Cleanup] Scheduled cleanup failed:', error);
+ console.log('========================================');
+ throw error;
+ }
+ }
+
+ // Berechnet verbleibende Tage bis zur Löschung
+ getDaysUntilDeletion(uploadDate) {
+ const upload = new Date(uploadDate);
+ const deleteDate = new Date(upload);
+ deleteDate.setDate(deleteDate.getDate() + this.CLEANUP_DAYS);
+
+ const now = new Date();
+ const diffTime = deleteDate - now;
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ return Math.max(0, diffDays);
+ }
+}
+
+module.exports = new GroupCleanupService();
diff --git a/backend/src/services/SchedulerService.js b/backend/src/services/SchedulerService.js
new file mode 100644
index 0000000..51c8b8a
--- /dev/null
+++ b/backend/src/services/SchedulerService.js
@@ -0,0 +1,49 @@
+const cron = require('node-cron');
+const GroupCleanupService = require('./GroupCleanupService');
+
+class SchedulerService {
+ constructor() {
+ this.tasks = [];
+ }
+
+ start() {
+ console.log('[Scheduler] Starting scheduled tasks...');
+
+ // Cleanup-Job: Jeden Tag um 10:00 Uhr
+ const cleanupTask = cron.schedule('0 10 * * *', async () => {
+ console.log('[Scheduler] Running daily cleanup at 10:00 AM...');
+ try {
+ await GroupCleanupService.performScheduledCleanup();
+ } catch (error) {
+ console.error('[Scheduler] Cleanup task failed:', error);
+ }
+ }, {
+ scheduled: true,
+ timezone: "Europe/Berlin" // Anpassen nach Bedarf
+ });
+
+ this.tasks.push(cleanupTask);
+
+ console.log('✓ Scheduler started - Daily cleanup at 10:00 AM (Europe/Berlin)');
+
+ // Für Development: Manueller Trigger
+ if (process.env.NODE_ENV === 'development') {
+ console.log('📝 Development Mode: Use GroupCleanupService.performScheduledCleanup() to trigger manually');
+ }
+ }
+
+ stop() {
+ console.log('[Scheduler] Stopping all scheduled tasks...');
+ this.tasks.forEach(task => task.stop());
+ this.tasks = [];
+ console.log('✓ Scheduler stopped');
+ }
+
+ // Für Development: Manueller Cleanup-Trigger
+ async triggerCleanupNow() {
+ console.log('[Scheduler] Manual cleanup triggered...');
+ return await GroupCleanupService.performScheduledCleanup();
+ }
+}
+
+module.exports = new SchedulerService();
diff --git a/docker/dev/frontend/nginx.conf b/docker/dev/frontend/nginx.conf
index 5da7c6a..4a52652 100644
--- a/docker/dev/frontend/nginx.conf
+++ b/docker/dev/frontend/nginx.conf
@@ -55,6 +55,15 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ # Admin API routes (NO password protection - protected by /moderation page access)
+ location /api/admin {
+ proxy_pass http://backend-dev:5000/api/admin;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# Protected API - Moderation API routes (password protected) - must come before /groups
location /moderation/groups {
auth_basic "Restricted Area - Moderation API";
diff --git a/docker/prod/frontend/nginx.conf b/docker/prod/frontend/nginx.conf
index 8464f72..523a6fa 100644
--- a/docker/prod/frontend/nginx.conf
+++ b/docker/prod/frontend/nginx.conf
@@ -89,6 +89,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ # Admin API routes (NO password protection - protected by /moderation page access)
+ location /api/admin {
+ proxy_pass http://image-uploader-backend:5000/api/admin;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# Protected API - Moderation API routes (password protected) - must come before /groups
location /moderation/groups {
auth_basic "Restricted Area - Moderation API";
diff --git a/docs/FEATURE_PLAN-delete-unproved-groups.md b/docs/FEATURE_PLAN-delete-unproved-groups.md
new file mode 100644
index 0000000..69b5b48
--- /dev/null
+++ b/docs/FEATURE_PLAN-delete-unproved-groups.md
@@ -0,0 +1,655 @@
+# Feature Plan: Automatisches Löschen nicht freigegebener Gruppen
+
+## 📋 Übersicht
+
+**Feature**: Automatisches Löschen von nicht freigegebenen Gruppen nach 7 Tagen
+**Ziel**: Verhindern, dass rechtlich oder sozial anstößige Inhalte dauerhaft auf dem Server verbleiben
+**Priorität**: Hoch (Sicherheit & Compliance)
+**Geschätzte Implementierungszeit**: 2-3 Tage
+
+## 🎯 Funktionale Anforderungen
+
+### Must-Have
+- [ ] **Automatische Löschung**: Gruppen mit `approved = false` werden nach 7 Tagen ab Upload-Zeitpunkt gelöscht
+- [ ] **Vollständige Löschung**: Datenbank-Einträge, Originalbilder und Preview-Bilder werden entfernt
+- [ ] **Cron-Job**: Tägliche Ausführung um 10:00 Uhr morgens
+- [ ] **Deletion Log**: Protokollierung gelöschter Gruppen in eigener Datenbanktabelle
+- [ ] **Anonymisierung**: Keine personenbezogenen Daten (Titel, Name, Beschreibung) im Log
+- [ ] **Countdown-Anzeige**: In ModerationPage wird Restzeit bis zur Löschung angezeigt
+- [ ] **Admin-Übersicht**: Geschützter Bereich in ModerationPage für Lösch-Historie
+- [ ] **Freigabe-Schutz**: Freigegebene Gruppen (`approved = true`) werden niemals automatisch gelöscht
+
+### Nice-to-Have
+- [ ] **Manuelle Verzögerung**: Admin kann Löschfrist verlängern (z.B. um weitere 7 Tage)
+- [ ] **Batch-Delete Preview**: Vorschau welche Gruppen beim nächsten Cron-Lauf gelöscht würden
+- [ ] **Email-Benachrichtigung**: Warnung an Admin 24h vor automatischer Löschung
+
+## 🔧 Technische Umsetzung
+
+### 1. Database Schema Erweiterung
+
+#### 1.1 Groups-Tabelle Status ✅ **BEREITS VORHANDEN**
+**Datei**: `backend/src/database/DatabaseManager.js`
+
+**Status:** Die `approved` Spalte existiert bereits!
+```javascript
+// Zeile 60-63 in DatabaseManager.js
+CREATE TABLE IF NOT EXISTS groups (
+ // ...
+ approved BOOLEAN DEFAULT FALSE,
+ // ...
+)
+```
+
+**Migration:** Wird automatisch bei jedem Server-Start ausgeführt (Zeile 67-75):
+```javascript
+try {
+ await this.run('ALTER TABLE groups ADD COLUMN approved BOOLEAN DEFAULT FALSE');
+} catch (error) {
+ // Feld existiert bereits - das ist okay
+}
+```
+
+**Zusätzlicher Index für Performance (neu hinzufügen):**
+```sql
+CREATE INDEX IF NOT EXISTS idx_groups_cleanup ON groups(approved, upload_date);
+CREATE INDEX IF NOT EXISTS idx_groups_approved ON groups(approved);
+```
+
+#### 1.2 Neue Tabelle: Deletion Log
+**Datei**: `backend/src/database/schema.sql`
+
+```sql
+-- Deletion Log für gelöschte Gruppen (Compliance & Audit Trail)
+CREATE TABLE IF NOT EXISTS deletion_log (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ group_id TEXT NOT NULL, -- Original Group ID (zur Referenz)
+ year INTEGER NOT NULL, -- Jahr des Uploads
+ image_count INTEGER NOT NULL, -- Anzahl gelöschter Bilder
+ upload_date DATETIME NOT NULL, -- Ursprünglicher Upload-Zeitpunkt
+ deleted_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- Zeitpunkt der Löschung
+ deletion_reason TEXT DEFAULT 'auto_cleanup_7days', -- Grund der Löschung
+ total_file_size INTEGER -- Gesamtgröße der gelöschten Dateien (in Bytes)
+);
+
+-- Index für schnelle Abfragen nach Löschdatum
+CREATE INDEX IF NOT EXISTS idx_deletion_log_deleted_at ON deletion_log(deleted_at DESC);
+
+-- Index für Jahresfilterung
+CREATE INDEX IF NOT EXISTS idx_deletion_log_year ON deletion_log(year);
+```
+
+**Wichtig**: Keine personenbezogenen Daten (title, name, description) werden gespeichert!
+
+### 2. Backend-Implementierung
+
+#### 2.1 Migration Script
+**Datei**: `backend/src/database/migrations/005_add_approved_column.sql` (neu erstellen)
+
+```sql
+-- Migration 005: Add approved column to groups table
+ALTER TABLE groups ADD COLUMN approved BOOLEAN DEFAULT FALSE;
+
+-- Index für schnelle Abfragen nicht freigegebener Gruppen
+CREATE INDEX IF NOT EXISTS idx_groups_approved ON groups(approved);
+
+-- Index für Lösch-Kandidaten (approved=false + alte upload_date)
+CREATE INDEX IF NOT EXISTS idx_groups_cleanup ON groups(approved, upload_date);
+```
+
+#### 2.2 Cleanup Service
+**Datei**: `backend/src/services/GroupCleanupService.js` (neu erstellen)
+
+**Verantwortlichkeiten:**
+- Identifizierung löschbarer Gruppen (nicht freigegeben + älter als 7 Tage)
+- Berechnung der Löschfrist pro Gruppe
+- Vollständige Löschung (DB + Dateien)
+- Protokollierung in `deletion_log`
+
+**Hauptmethoden:**
+```javascript
+class GroupCleanupService {
+ // Findet alle Gruppen, die gelöscht werden müssen
+ async findGroupsForDeletion()
+
+ // Löscht eine Gruppe vollständig (Transaktion)
+ async deleteGroupCompletely(groupId)
+
+ // Erstellt Eintrag im Deletion Log
+ async logDeletion(groupData)
+
+ // Hauptmethode: Führt kompletten Cleanup durch
+ async performScheduledCleanup()
+
+ // Berechnet verbleibende Tage bis zur Löschung
+ getDaysUntilDeletion(uploadDate)
+}
+```
+
+#### 2.3 Repository-Erweiterungen
+**Datei**: `backend/src/repositories/GroupRepository.js`
+
+**Bestehende Methoden (werden wiederverwendet):** ✅
+```javascript
+// ✅ BEREITS VORHANDEN - Zeile 207
+async updateGroupApproval(groupId, approved) { }
+
+// ✅ BEREITS VORHANDEN - Zeile 217
+async deleteImage(groupId, imageId) { }
+```
+
+**Neue Methoden:**
+```javascript
+// Findet Gruppen, die zum Löschen anstehen (approved=false & älter als 7 Tage)
+async findUnapprovedGroupsOlderThan(days) { }
+
+// Löscht Gruppe komplett (inkl. Bilder-Referenzen) - erweitert bestehende Logik
+async deleteGroupCompletely(groupId) { }
+
+// Hole Statistiken für Gruppe (für Deletion Log)
+async getGroupStatistics(groupId) { }
+```
+
+**Datei**: `backend/src/repositories/DeletionLogRepository.js` (neu erstellen)
+
+```javascript
+class DeletionLogRepository {
+ // Erstellt Lösch-Protokoll
+ async createDeletionEntry(logData) { }
+
+ // Hole letzte N Einträge
+ async getRecentDeletions(limit = 10) { }
+
+ // Hole alle Einträge (für Admin-Übersicht)
+ async getAllDeletions() { }
+
+ // Statistiken (Anzahl gelöschte Gruppen, Bilder, Speicherplatz)
+ async getDeletionStatistics() { }
+}
+```
+
+#### 2.4 Cron-Job Implementation
+**Datei**: `backend/src/services/SchedulerService.js` (neu erstellen)
+
+**Library**: `node-cron`
+```bash
+cd backend
+npm install node-cron
+```
+
+**Implementation:**
+```javascript
+const cron = require('node-cron');
+const GroupCleanupService = require('./GroupCleanupService');
+
+class SchedulerService {
+ start() {
+ // Jeden Tag um 10:00 Uhr
+ cron.schedule('0 10 * * *', async () => {
+ console.log('[Scheduler] Running daily cleanup at 10:00 AM...');
+ await GroupCleanupService.performScheduledCleanup();
+ });
+ }
+}
+```
+
+**Integration in**: `backend/src/server.js`
+```javascript
+const SchedulerService = require('./services/SchedulerService');
+
+// Nach Server-Start
+app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+
+ // Starte Scheduler
+ const scheduler = new SchedulerService();
+ scheduler.start();
+});
+```
+
+#### 2.5 API-Endpunkte
+
+**Route**: `backend/src/routes/groups.js`
+
+**Bestehender Endpoint (wird wiederverwendet):** ✅
+```javascript
+// ✅ BEREITS VORHANDEN - Zeile 102
+PATCH /groups/:groupId/approve
+Body: { approved: true/false }
+Response: { success: true, message: "Gruppe freigegeben", approved: true }
+```
+
+**Neue Admin-Endpunkte:**
+```javascript
+// Neu: Hole Deletion Log
+GET /api/admin/deletion-log?limit=10
+Response: { deletions: [...], total: 123 }
+
+// Neu: Hole alle Deletion Logs
+GET /api/admin/deletion-log/all
+Response: { deletions: [...] }
+
+// Neu: Deletion Statistiken
+GET /api/admin/deletion-log/stats
+Response: {
+ totalDeleted: 45,
+ totalImages: 234,
+ totalSize: '1.2 GB',
+ lastCleanup: '2025-11-08T10:00:00Z'
+}
+```
+
+### 3. Frontend-Implementierung
+
+#### 3.1 ModerationGroupPage Erweiterungen
+**Datei**: `frontend/src/Components/Pages/ModerationGroupPage.js`
+
+**Neue Features:**
+- Countdown-Anzeige für jede nicht freigegebene Gruppe
+- Farbcodierung optional (aktuell nicht gewünscht)
+- Button "Gruppe freigeben" (approved setzen)
+
+**UI-Änderungen:**
+```jsx
+
+
+
+