fix: GUI improvements and fixes
Mirror to GitHub / mirror (push) Successful in 19s
Test and Lint / backend-test (push) Successful in 1m12s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m6s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 2s

- Enable JSON module imports in TypeScript config to fix frontend version display
- Fix archive page showing '00' instead of '0' for empty photo counts
- Add null safety to archive photo count calculation
- Update email processor to reinitialize on config changes
- Add auto-retry for failed email transporter initialization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-16 10:18:28 +02:00
parent 76ae35217c
commit cfa29ad5cb
9 changed files with 115 additions and 10 deletions
Binary file not shown.
+35 -3
View File
@@ -3,9 +3,17 @@ const { db } = require('../database/db');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
let transporter = null; 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 // Initialize transporter from database config
async function initializeTransporter() { async function initializeTransporter(forceReinit = false) {
try { try {
const config = await db('email_configs').first(); const config = await db('email_configs').first();
@@ -14,6 +22,16 @@ async function initializeTransporter() {
return null; 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({ transporter = nodemailer.createTransport({
host: config.smtp_host, host: config.smtp_host,
port: config.smtp_port, port: config.smtp_port,
@@ -28,9 +46,14 @@ async function initializeTransporter() {
await transporter.verify(); await transporter.verify();
logger.info('Email transporter initialized successfully'); logger.info('Email transporter initialized successfully');
// Update the config hash
lastConfigHash = currentConfigHash;
return transporter; return transporter;
} catch (error) { } catch (error) {
logger.error('Failed to initialize email transporter:', error); logger.error('Failed to initialize email transporter:', error);
transporter = null;
lastConfigHash = null;
return null; return null;
} }
} }
@@ -256,12 +279,11 @@ async function processTemplate(template, variables, language = 'en') {
// Send email using template // Send email using template
async function sendTemplateEmail(to, templateKey, variables) { async function sendTemplateEmail(to, templateKey, variables) {
try { try {
if (!transporter) { // Always check for configuration changes before sending
transporter = await initializeTransporter(); transporter = await initializeTransporter();
if (!transporter) { if (!transporter) {
throw new Error('Email service not configured'); throw new Error('Email service not configured');
} }
}
// Get email template // Get email template
const template = await db('email_templates') const template = await db('email_templates')
@@ -304,6 +326,16 @@ async function sendTemplateEmail(to, templateKey, variables) {
// Process email queue // Process email queue
async function processEmailQueue() { async function processEmailQueue() {
try { 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') const pendingEmails = await db('email_queue')
.where('status', 'pending') .where('status', 'pending')
.where('retry_count', '<', 3) .where('retry_count', '<', 3)
@@ -3,9 +3,10 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { api } from '../../config/api'; import { api } from '../../config/api';
import packageJson from '../../../package.json';
// Frontend version from package.json // Frontend version from package.json
const FRONTEND_VERSION = '1.0.0'; const FRONTEND_VERSION = packageJson.version;
interface SystemVersion { interface SystemVersion {
backend: string; backend: string;
+1 -1
View File
@@ -58,7 +58,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
{...props} {...props}
/> />
{rightIcon && ( {rightIcon && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<span className="text-neutral-500">{rightIcon}</span> <span className="text-neutral-500">{rightIcon}</span>
</div> </div>
)} )}
+8 -1
View File
@@ -264,6 +264,7 @@
"adminEmailHelp": "Erhält Systembenachrichtigungen und Archivbestätigungen", "adminEmailHelp": "Erhält Systembenachrichtigungen und Archivbestätigungen",
"securityAccess": "Sicherheit & Zugriff", "securityAccess": "Sicherheit & Zugriff",
"galleryPassword": "Galerie-Passwort", "galleryPassword": "Galerie-Passwort",
"passwordHelperText": "Sie können Datumsangaben wie \"04.07.2025\" oder beliebigen Text mit mindestens 6 Zeichen verwenden",
"passwordPlaceholder": "Sicheres Passwort eingeben", "passwordPlaceholder": "Sicheres Passwort eingeben",
"confirmPassword": "Passwort bestätigen", "confirmPassword": "Passwort bestätigen",
"showPasswords": "Passwörter anzeigen", "showPasswords": "Passwörter anzeigen",
@@ -363,7 +364,13 @@
"eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt", "eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt",
"bulkArchive": "Archivieren", "bulkArchive": "Archivieren",
"confirmBulkArchive": "Sind Sie sicher, dass Sie {{count}} Veranstaltung(en) archivieren möchten?", "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": { "settings": {
"title": "Systemeinstellungen", "title": "Systemeinstellungen",
+7
View File
@@ -282,6 +282,7 @@
"adminEmailHelp": "Will receive system notifications and archive confirmations", "adminEmailHelp": "Will receive system notifications and archive confirmations",
"securityAccess": "Security & Access", "securityAccess": "Security & Access",
"galleryPassword": "Gallery Password", "galleryPassword": "Gallery Password",
"passwordHelperText": "You can use dates like \"04.07.2025\" or any text with 6+ characters",
"confirmPassword": "Confirm Password", "confirmPassword": "Confirm Password",
"showPasswords": "Show passwords", "showPasswords": "Show passwords",
"gallerySettings": "Gallery Settings", "gallerySettings": "Gallery Settings",
@@ -340,6 +341,12 @@
"expires": "Expires", "expires": "Expires",
"actions": "Actions", "actions": "Actions",
"noEventsFound": "No events found", "noEventsFound": "No events found",
"stats": {
"totalEvents": "Total Events",
"activeEvents": "Active Events",
"totalPhotos": "Total Photos",
"expiringEvents": "Expiring Soon"
},
"viewDetails": "View Details", "viewDetails": "View Details",
"archiveEventAction": "Archive Event", "archiveEventAction": "Archive Event",
"downloadArchiveAction": "Download Archive", "downloadArchiveAction": "Download Archive",
+1 -1
View File
@@ -169,7 +169,7 @@ export const ArchivesPage: React.FC = () => {
<div> <div>
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p> <p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
<p className="text-2xl font-bold text-neutral-900"> <p className="text-2xl font-bold text-neutral-900">
{archives.reduce((sum, a) => sum + a.photoCount, 0).toLocaleString()} {archives.reduce((sum, a) => sum + (a.photoCount || 0), 0).toLocaleString()}
</p> </p>
</div> </div>
<FileArchive className="w-8 h-8 text-green-600" /> <FileArchive className="w-8 h-8 text-green-600" />
+58 -1
View File
@@ -9,7 +9,11 @@ import {
ExternalLink, ExternalLink,
Edit, Edit,
Download, Download,
Trash2 Trash2,
Calendar,
Users,
Image,
Activity
} from 'lucide-react'; } from 'lucide-react';
import { parseISO, differenceInDays } from 'date-fns'; import { parseISO, differenceInDays } from 'date-fns';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -232,6 +236,59 @@ export const EventsListPage: React.FC = () => {
</Button> </Button>
</div> </div>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('events.stats.totalEvents')}</p>
<p className="text-2xl font-bold text-neutral-900">{data?.events.length || 0}</p>
</div>
<Calendar className="w-8 h-8 text-primary-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('events.stats.activeEvents')}</p>
<p className="text-2xl font-bold text-neutral-900">
{data?.events.filter(e => e.is_active && !e.is_archived).length || 0}
</p>
</div>
<Activity className="w-8 h-8 text-green-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('events.stats.totalPhotos')}</p>
<p className="text-2xl font-bold text-neutral-900">
{data?.events.reduce((sum, e) => sum + (e.photo_count || 0), 0) || 0}
</p>
</div>
<Image className="w-8 h-8 text-blue-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('events.stats.expiringEvents')}</p>
<p className="text-2xl font-bold text-neutral-900">
{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}
</p>
</div>
<AlertTriangle className="w-8 h-8 text-orange-600" />
</div>
</Card>
</div>
{/* Filters and Search */} {/* Filters and Search */}
<Card padding="sm" className="mb-6"> <Card padding="sm" className="mb-6">
<div className="flex flex-col lg:flex-row gap-4"> <div className="flex flex-col lg:flex-row gap-4">
+1
View File
@@ -10,6 +10,7 @@
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": false, "verbatimModuleSyntax": false,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,