diff --git a/backend/data/photo_sharing.db b/backend/data/photo_sharing.db index 5462443..6e2ef79 100644 Binary files a/backend/data/photo_sharing.db and b/backend/data/photo_sharing.db differ diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index f68d9f9..2d703c2 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -3,9 +3,17 @@ const { db } = require('../database/db'); const logger = require('../utils/logger'); let transporter = null; +let lastConfigHash = null; + +// Generate hash from config for change detection +function generateConfigHash(config) { + const crypto = require('crypto'); + const configString = `${config.smtp_host}:${config.smtp_port}:${config.smtp_user}:${config.smtp_pass}:${config.smtp_secure}`; + return crypto.createHash('md5').update(configString).digest('hex'); +} // Initialize transporter from database config -async function initializeTransporter() { +async function initializeTransporter(forceReinit = false) { try { const config = await db('email_configs').first(); @@ -14,6 +22,16 @@ async function initializeTransporter() { return null; } + // Check if configuration has changed + const currentConfigHash = generateConfigHash(config); + if (!forceReinit && transporter && currentConfigHash === lastConfigHash) { + // Configuration hasn't changed, return existing transporter + return transporter; + } + + // Configuration has changed or first initialization + logger.info('Initializing email transporter' + (lastConfigHash && currentConfigHash !== lastConfigHash ? ' (configuration changed)' : '')); + transporter = nodemailer.createTransport({ host: config.smtp_host, port: config.smtp_port, @@ -28,9 +46,14 @@ async function initializeTransporter() { await transporter.verify(); logger.info('Email transporter initialized successfully'); + // Update the config hash + lastConfigHash = currentConfigHash; + return transporter; } catch (error) { logger.error('Failed to initialize email transporter:', error); + transporter = null; + lastConfigHash = null; return null; } } @@ -256,11 +279,10 @@ async function processTemplate(template, variables, language = 'en') { // Send email using template async function sendTemplateEmail(to, templateKey, variables) { try { + // Always check for configuration changes before sending + transporter = await initializeTransporter(); if (!transporter) { - transporter = await initializeTransporter(); - if (!transporter) { - throw new Error('Email service not configured'); - } + throw new Error('Email service not configured'); } // Get email template @@ -304,6 +326,16 @@ async function sendTemplateEmail(to, templateKey, variables) { // Process email queue async function processEmailQueue() { try { + // Try to initialize transporter if it's null (in case it failed at startup) + if (!transporter) { + logger.info('Transporter not initialized, attempting to initialize...'); + transporter = await initializeTransporter(); + if (!transporter) { + logger.warn('Email transporter could not be initialized, skipping queue processing'); + return; + } + } + const pendingEmails = await db('email_queue') .where('status', 'pending') .where('retry_count', '<', 3) diff --git a/frontend/src/components/admin/VersionInfo.tsx b/frontend/src/components/admin/VersionInfo.tsx index e2e3891..18f8d1e 100644 --- a/frontend/src/components/admin/VersionInfo.tsx +++ b/frontend/src/components/admin/VersionInfo.tsx @@ -3,9 +3,10 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Info } from 'lucide-react'; import { api } from '../../config/api'; +import packageJson from '../../../package.json'; // Frontend version from package.json -const FRONTEND_VERSION = '1.0.0'; +const FRONTEND_VERSION = packageJson.version; interface SystemVersion { backend: string; diff --git a/frontend/src/components/common/Input.tsx b/frontend/src/components/common/Input.tsx index 67f18b0..51a3f40 100644 --- a/frontend/src/components/common/Input.tsx +++ b/frontend/src/components/common/Input.tsx @@ -58,7 +58,7 @@ export const Input = React.forwardRef( {...props} /> {rightIcon && ( -
+
{rightIcon}
)} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 0b2b458..679976d 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -264,6 +264,7 @@ "adminEmailHelp": "Erhält Systembenachrichtigungen und Archivbestätigungen", "securityAccess": "Sicherheit & Zugriff", "galleryPassword": "Galerie-Passwort", + "passwordHelperText": "Sie können Datumsangaben wie \"04.07.2025\" oder beliebigen Text mit mindestens 6 Zeichen verwenden", "passwordPlaceholder": "Sicheres Passwort eingeben", "confirmPassword": "Passwort bestätigen", "showPasswords": "Passwörter anzeigen", @@ -363,7 +364,13 @@ "eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt", "bulkArchive": "Archivieren", "confirmBulkArchive": "Sind Sie sicher, dass Sie {{count}} Veranstaltung(en) archivieren möchten?", - "confirmBulkArchiveDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Archivierte Veranstaltungen sind nicht mehr öffentlich zugänglich." + "confirmBulkArchiveDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Archivierte Veranstaltungen sind nicht mehr öffentlich zugänglich.", + "stats": { + "totalEvents": "Gesamtveranstaltungen", + "activeEvents": "Aktive Veranstaltungen", + "totalPhotos": "Gesamtfotos", + "expiringEvents": "Bald ablaufend" + } }, "settings": { "title": "Systemeinstellungen", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index b4edb70..e7a3eca 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -282,6 +282,7 @@ "adminEmailHelp": "Will receive system notifications and archive confirmations", "securityAccess": "Security & Access", "galleryPassword": "Gallery Password", + "passwordHelperText": "You can use dates like \"04.07.2025\" or any text with 6+ characters", "confirmPassword": "Confirm Password", "showPasswords": "Show passwords", "gallerySettings": "Gallery Settings", @@ -340,6 +341,12 @@ "expires": "Expires", "actions": "Actions", "noEventsFound": "No events found", + "stats": { + "totalEvents": "Total Events", + "activeEvents": "Active Events", + "totalPhotos": "Total Photos", + "expiringEvents": "Expiring Soon" + }, "viewDetails": "View Details", "archiveEventAction": "Archive Event", "downloadArchiveAction": "Download Archive", diff --git a/frontend/src/pages/admin/ArchivesPage.tsx b/frontend/src/pages/admin/ArchivesPage.tsx index 8e1599c..2431511 100644 --- a/frontend/src/pages/admin/ArchivesPage.tsx +++ b/frontend/src/pages/admin/ArchivesPage.tsx @@ -169,7 +169,7 @@ export const ArchivesPage: React.FC = () => {

{t('archives.totalPhotos')}

- {archives.reduce((sum, a) => sum + a.photoCount, 0).toLocaleString()} + {archives.reduce((sum, a) => sum + (a.photoCount || 0), 0).toLocaleString()}

diff --git a/frontend/src/pages/admin/EventsListPage.tsx b/frontend/src/pages/admin/EventsListPage.tsx index 5abd66a..44c4947 100644 --- a/frontend/src/pages/admin/EventsListPage.tsx +++ b/frontend/src/pages/admin/EventsListPage.tsx @@ -9,7 +9,11 @@ import { ExternalLink, Edit, Download, - Trash2 + Trash2, + Calendar, + Users, + Image, + Activity } from 'lucide-react'; import { parseISO, differenceInDays } from 'date-fns'; import { toast } from 'react-toastify'; @@ -232,6 +236,59 @@ export const EventsListPage: React.FC = () => {
+ {/* Statistics Cards */} +
+ +
+
+

{t('events.stats.totalEvents')}

+

{data?.events.length || 0}

+
+ +
+
+ + +
+
+

{t('events.stats.activeEvents')}

+

+ {data?.events.filter(e => e.is_active && !e.is_archived).length || 0} +

+
+ +
+
+ + +
+
+

{t('events.stats.totalPhotos')}

+

+ {data?.events.reduce((sum, e) => sum + (e.photo_count || 0), 0) || 0} +

+
+ +
+
+ + +
+
+

{t('events.stats.expiringEvents')}

+

+ {data?.events.filter(e => { + if (!e.is_active || e.is_archived) return false; + const days = e.expires_at ? differenceInDays(parseISO(e.expires_at), new Date()) : 0; + return days <= 7 && days > 0; + }).length || 0} +

+
+ +
+
+
+ {/* Filters and Search */}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 028c24e..c7e3da4 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -10,6 +10,7 @@ /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, + "resolveJsonModule": true, "verbatimModuleSyntax": false, "moduleDetection": "force", "noEmit": true,