refactor: Move deletion log into ModerationGroupsPage

- Create DeletionLogSection component
- Integrate deletion log at bottom of moderation page
- Remove standalone DeletionLogPage and route
- Remove admin nav link (log now in moderation)
- Keep /api/admin routes for backend API access
- Update nginx configs (remove /admin frontend route)
This commit is contained in:
Matthias Lotz 2025-11-08 12:55:55 +01:00
parent 0f430af877
commit 3a2efd97c3
7 changed files with 271 additions and 317 deletions

View File

@ -107,20 +107,6 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Protected routes - Admin (password protected) - React Dev Server
location /admin {
auth_basic "Restricted Area - Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Protected routes - Moderation (password protected) - React Dev Server
location /moderation {
auth_basic "Restricted Area - Moderation";

View File

@ -141,20 +141,6 @@ http {
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
}
# Protected routes - Admin (password protected)
location /admin {
auth_basic "Restricted Area - Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
expires -1;
# Prevent indexing
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
}
# Protected routes - Moderation (password protected)
location /moderation {
auth_basic "Restricted Area - Moderation";

View File

@ -8,7 +8,6 @@ import GroupsOverviewPage from './Components/Pages/GroupsOverviewPage';
import ModerationGroupsPage from './Components/Pages/ModerationGroupsPage';
import ModerationGroupImagesPage from './Components/Pages/ModerationGroupImagesPage';
import PublicGroupImagesPage from './Components/Pages/PublicGroupImagesPage';
import DeletionLogPage from './Components/Pages/DeletionLogPage';
import FZF from './Components/Pages/404Page.js'
function App() {
@ -21,7 +20,6 @@ function App() {
<Route path="/groups" element={<GroupsOverviewPage />} />
<Route path="/moderation" exact element={<ModerationGroupsPage />} />
<Route path="/moderation/groups/:groupId" element={<ModerationGroupImagesPage />} />
<Route path="/admin/deletion-log" element={<DeletionLogPage />} />
<Route path="*" element={<FZF />} />
</Routes>
</Router>

View File

@ -0,0 +1,264 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Typography,
Button,
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Grid,
CircularProgress
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import WarningIcon from '@mui/icons-material/Warning';
import InfoIcon from '@mui/icons-material/Info';
const DeletionLogSection = () => {
const [deletions, setDeletions] = useState([]);
const [statistics, setStatistics] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
loadDeletionLog();
loadStatistics();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showAll]);
const loadDeletionLog = async () => {
try {
setLoading(true);
const endpoint = showAll
? '/api/admin/deletion-log/all'
: '/api/admin/deletion-log?limit=10';
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setDeletions(data.deletions || []);
setError(null);
} catch (error) {
console.error('Fehler beim Laden des Lösch-Logs:', error);
setError('Fehler beim Laden des Lösch-Logs');
} finally {
setLoading(false);
}
};
const loadStatistics = async () => {
try {
const response = await fetch('/api/admin/deletion-log/stats');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setStatistics(data.statistics || null);
} catch (error) {
console.error('Fehler beim Laden der Statistiken:', error);
}
};
const formatDate = (dateString) => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '0 KB';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
};
const getReasonIcon = (reason) => {
if (reason && reason.includes('unapproved')) {
return <WarningIcon fontSize="small" color="warning" />;
}
return <DeleteIcon fontSize="small" color="action" />;
};
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<CircularProgress size={40} />
<Typography variant="body2" sx={{ mt: 2, color: '#666' }}>
Lade Lösch-Historie...
</Typography>
</Box>
);
}
return (
<Box sx={{ mt: 6, mb: 4 }}>
<Box sx={{ mb: 3 }}>
<Typography variant="h5" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DeleteIcon />
Lösch-Historie
</Typography>
<Typography variant="body2" color="text.secondary">
Automatisch gelöschte Gruppen (nicht innerhalb von 7 Tagen freigegeben)
</Typography>
</Box>
{error && (
<Card sx={{ p: 2, mb: 3, backgroundColor: '#ffebee' }}>
<Typography color="error">{error}</Typography>
</Card>
)}
{/* Statistics Cards */}
{statistics && (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={4}>
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h4" color="primary" fontWeight="bold">
{statistics.totalGroupsDeleted || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Gelöschte Gruppen
</Typography>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h4" color="secondary" fontWeight="bold">
{statistics.totalImagesDeleted || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Gelöschte Bilder
</Typography>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card sx={{ p: 2, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h4" color="success.main" fontWeight="bold">
{statistics.totalStorageFreed || '0 KB'}
</Typography>
<Typography variant="body2" color="text.secondary">
Speicher freigegeben
</Typography>
</Card>
</Grid>
</Grid>
)}
{/* Toggle Button */}
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="subtitle1" fontWeight="bold">
{showAll ? 'Alle Einträge' : 'Letzte 10 Einträge'}
</Typography>
<Button
variant="outlined"
size="small"
onClick={() => setShowAll(!showAll)}
startIcon={<InfoIcon />}
>
{showAll ? 'Nur letzte 10' : 'Alle anzeigen'}
</Button>
</Box>
{/* Deletion Log Table */}
{deletions.length === 0 ? (
<Card sx={{ p: 3, textAlign: 'center' }}>
<InfoIcon sx={{ fontSize: 48, color: '#bdbdbd', mb: 1 }} />
<Typography variant="body1" color="text.secondary">
Keine Lösch-Einträge gefunden
</Typography>
<Typography variant="body2" color="text.secondary">
Es wurden bisher keine Gruppen automatisch gelöscht.
</Typography>
</Card>
) : (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow sx={{ backgroundColor: '#f5f5f5' }}>
<TableCell><strong>Gruppe ID</strong></TableCell>
<TableCell><strong>Jahr</strong></TableCell>
<TableCell align="right"><strong>Bilder</strong></TableCell>
<TableCell><strong>Upload-Datum</strong></TableCell>
<TableCell><strong>Gelöscht am</strong></TableCell>
<TableCell><strong>Grund</strong></TableCell>
<TableCell align="right"><strong>Größe</strong></TableCell>
</TableRow>
</TableHead>
<TableBody>
{deletions.map((row) => (
<TableRow
key={row.id}
sx={{ '&:hover': { backgroundColor: '#fafafa' } }}
>
<TableCell>
<Chip
label={row.group_id}
size="small"
variant="outlined"
/>
</TableCell>
<TableCell>{row.year || '-'}</TableCell>
<TableCell align="right">{row.image_count || 0}</TableCell>
<TableCell>{formatDate(row.upload_date)}</TableCell>
<TableCell>{formatDate(row.deleted_at)}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getReasonIcon(row.deletion_reason)}
<Typography variant="body2">
{row.deletion_reason || 'Unbekannt'}
</Typography>
</Box>
</TableCell>
<TableCell align="right">
<Typography variant="body2" color="text.secondary">
{formatFileSize(row.total_file_size)}
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{/* Info Box */}
<Card sx={{ mt: 3, p: 2, backgroundColor: '#e3f2fd' }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<InfoIcon color="info" />
<Box>
<Typography variant="body2" fontWeight="bold" gutterBottom>
Automatische Löschung
</Typography>
<Typography variant="body2" color="text.secondary">
Der Cleanup läuft täglich um 10:00 Uhr. Gruppen, die nicht innerhalb von 7 Tagen
freigegeben werden, werden automatisch gelöscht. Alle Lösch-Vorgänge werden hier protokolliert.
</Typography>
</Box>
</Box>
</Card>
</Box>
);
};
export default DeletionLogSection;

View File

@ -4,7 +4,7 @@ import { NavLink } from 'react-router-dom'
import '../Css/Navbar.css'
import logo from '../../../Images/logo.png'
import { Lock as LockIcon, AdminPanelSettings as AdminIcon } from '@mui/icons-material';
import { Lock as LockIcon } from '@mui/icons-material';
function Navbar() {
return (
@ -15,7 +15,6 @@ function Navbar() {
<li><NavLink to="/groups" activeClassName="active">Groups</NavLink></li>
<li><NavLink to="/slideshow" activeClassName="active">Slideshow</NavLink></li>
<li><NavLink to="/moderation" activeClassName="active"><LockIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Moderation</NavLink></li>
<li><NavLink to="/admin/deletion-log" activeClassName="active"><AdminIcon style={{ fontSize: 18, verticalAlign: 'text-bottom', marginRight: 6 }} aria-hidden="true" />Lösch-Log</NavLink></li>
<li><NavLink className="cta" exact to="/">Upload</NavLink></li>
<li><a href="https://www.hobbyhimmel.de/ueber-uns/konzept/">About</a></li>
</ul>

View File

@ -1,285 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import {
Container,
Card,
Typography,
Button,
Box,
CircularProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Grid
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import WarningIcon from '@mui/icons-material/Warning';
import InfoIcon from '@mui/icons-material/Info';
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
const DeletionLogPage = () => {
const [deletions, setDeletions] = useState([]);
const [statistics, setStatistics] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
loadDeletionLog();
loadStatistics();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showAll]);
const loadDeletionLog = async () => {
try {
setLoading(true);
const endpoint = showAll
? '/api/admin/deletion-log/all'
: '/api/admin/deletion-log?limit=10';
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setDeletions(data.deletions || []);
setError(null);
} catch (error) {
console.error('Fehler beim Laden des Lösch-Logs:', error);
setError('Fehler beim Laden des Lösch-Logs');
} finally {
setLoading(false);
}
};
const loadStatistics = async () => {
try {
const response = await fetch('/api/admin/deletion-log/stats');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setStatistics(data.statistics || null);
} catch (error) {
console.error('Fehler beim Laden der Statistiken:', error);
}
};
const formatDate = (dateString) => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
const getReasonIcon = (reason) => {
if (reason.includes('unapproved')) {
return <WarningIcon fontSize="small" color="warning" />;
}
return <DeleteIcon fontSize="small" color="action" />;
};
if (loading) {
return (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" className="page-container">
<div className="loading-container" style={{ textAlign: 'center', padding: '50px' }}>
<CircularProgress size={60} color="primary" />
<Typography variant="h6" style={{ marginTop: '20px', color: '#666666' }}>
Lade Lösch-Historie...
</Typography>
</div>
</Container>
<Footer />
</div>
);
}
return (
<div className="allContainer">
<Helmet>
<title>Lösch-Historie - Image Uploader</title>
</Helmet>
<Navbar />
<Container maxWidth="lg" className="page-container" style={{ marginTop: '30px', marginBottom: '30px' }}>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
<DeleteIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Lösch-Historie
</Typography>
<Typography variant="body1" color="text.secondary">
Übersicht über automatisch gelöschte Gruppen
</Typography>
</Box>
{error && (
<Card sx={{ p: 2, mb: 3, backgroundColor: '#ffebee' }}>
<Typography color="error">{error}</Typography>
</Card>
)}
{/* Statistics Cards */}
{statistics && (
<Grid container spacing={3} sx={{ mb: 4 }}>
<Grid item xs={12} md={4}>
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h3" color="primary" fontWeight="bold">
{statistics.totalGroupsDeleted || 0}
</Typography>
<Typography variant="body1" color="text.secondary">
Gelöschte Gruppen
</Typography>
</Card>
</Grid>
<Grid item xs={12} md={4}>
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h3" color="secondary" fontWeight="bold">
{statistics.totalImagesDeleted || 0}
</Typography>
<Typography variant="body1" color="text.secondary">
Gelöschte Bilder
</Typography>
</Card>
</Grid>
<Grid item xs={12} md={4}>
<Card sx={{ p: 3, textAlign: 'center', backgroundColor: '#f5f5f5' }}>
<Typography variant="h3" color="success.main" fontWeight="bold">
{statistics.totalStorageFreed || '0 KB'}
</Typography>
<Typography variant="body1" color="text.secondary">
Speicher freigegeben
</Typography>
</Card>
</Grid>
</Grid>
)}
{/* Toggle Button */}
<Box sx={{ mb: 3, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">
{showAll ? 'Alle Einträge' : 'Letzte 10 Einträge'}
</Typography>
<Button
variant="outlined"
onClick={() => setShowAll(!showAll)}
startIcon={<InfoIcon />}
>
{showAll ? 'Nur letzte 10 anzeigen' : 'Alle anzeigen'}
</Button>
</Box>
{/* Deletion Log Table */}
{deletions.length === 0 ? (
<Card sx={{ p: 4, textAlign: 'center' }}>
<InfoIcon sx={{ fontSize: 60, color: '#bdbdbd', mb: 2 }} />
<Typography variant="h6" color="text.secondary">
Keine Lösch-Einträge gefunden
</Typography>
<Typography variant="body2" color="text.secondary">
Es wurden bisher keine Gruppen automatisch gelöscht.
</Typography>
</Card>
) : (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow sx={{ backgroundColor: '#f5f5f5' }}>
<TableCell><strong>Gruppe ID</strong></TableCell>
<TableCell><strong>Jahr</strong></TableCell>
<TableCell align="right"><strong>Bilder</strong></TableCell>
<TableCell><strong>Upload-Datum</strong></TableCell>
<TableCell><strong>Gelöscht am</strong></TableCell>
<TableCell><strong>Grund</strong></TableCell>
<TableCell align="right"><strong>Größe</strong></TableCell>
</TableRow>
</TableHead>
<TableBody>
{deletions.map((row) => (
<TableRow
key={row.id}
sx={{
'&:hover': { backgroundColor: '#fafafa' },
'&:last-child td, &:last-child th': { border: 0 }
}}
>
<TableCell>
<Chip
label={row.group_id}
size="small"
variant="outlined"
/>
</TableCell>
<TableCell>{row.year || '-'}</TableCell>
<TableCell align="right">{row.image_count || 0}</TableCell>
<TableCell>{formatDate(row.upload_date)}</TableCell>
<TableCell>{formatDate(row.deleted_at)}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getReasonIcon(row.deletion_reason)}
<Typography variant="body2">
{row.deletion_reason || 'Unbekannt'}
</Typography>
</Box>
</TableCell>
<TableCell align="right">
<Typography variant="body2" color="text.secondary">
{formatFileSize(row.total_file_size)}
</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{/* Info Box */}
<Card sx={{ mt: 4, p: 3, backgroundColor: '#e3f2fd' }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<InfoIcon color="info" />
<Box>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
Hinweis zur automatischen Löschung
</Typography>
<Typography variant="body2" color="text.secondary">
Gruppen, die nicht innerhalb von 7 Tagen nach dem Upload freigegeben werden,
werden automatisch gelöscht. Der Cleanup läuft täglich um 10:00 Uhr.
Alle Lösch-Vorgänge werden hier protokolliert (ohne personenbezogene Daten).
</Typography>
</Box>
</Box>
</Card>
</Container>
<Footer />
</div>
);
};
// Helper function for file size formatting
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '0 KB';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
};
export default DeletionLogPage;

View File

@ -6,6 +6,7 @@ import Swal from 'sweetalert2/dist/sweetalert2.js';
import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import ImageGallery from '../ComponentUtils/ImageGallery';
import DeletionLogSection from '../ComponentUtils/DeletionLogSection';
import { getImageSrc } from '../../Utils/imageUtils';
const ModerationGroupsPage = () => {
@ -221,6 +222,11 @@ const ModerationGroupsPage = () => {
/>
</section>
{/* Lösch-Historie */}
<section className="moderation-section">
<DeletionLogSection />
</section>
{/* Bilder-Modal */}
{showImages && selectedGroup && (
<ImageModal