diff --git a/backend/migrations/core/087_add_update_notification_test_template.js b/backend/migrations/core/087_add_update_notification_test_template.js new file mode 100644 index 00000000..da337b45 --- /dev/null +++ b/backend/migrations/core/087_add_update_notification_test_template.js @@ -0,0 +1,130 @@ +/** + * Migration 087: Add a dedicated email template for the admin "Send Test + * Email" button on the Update Notifications settings page (#418). + * + * Previously the button reused the version_update_available template via + * sendUpdateNotificationNow(), which bailed out early when no real update + * was pending — so admins on the latest version had no way to verify + * their SMTP / recipient list config worked. + * + * The test template makes the intent unambiguous in the inbox ("This is + * a test of your update-notification setup, no action needed") and lets + * the send code run unconditionally regardless of update availability. + * + * Languages: EN + DE only, matching the convention of the existing + * version_update_available template (070). The email_templates table + * doesn't have nl/pt/ru columns; sendTemplateEmail falls back to EN. + */ + +exports.up = async function(knex) { + console.log('Running migration: 087_add_update_notification_test_template'); + + const existing = await knex('email_templates') + .where('template_key', 'version_update_test') + .first(); + + if (existing) { + console.log(' version_update_test template already exists, skipping insert'); + return; + } + + await knex('email_templates').insert({ + template_key: 'version_update_test', + subject_en: '[TEST] PicPeak Update Notification — configuration check', + subject_de: '[TEST] PicPeak Update-Benachrichtigung — Konfigurationsprüfung', + body_html_en: ` +

This is a test email

+ +

You are receiving this message because an administrator clicked +Send Test Email on the Update Notifications page of your +PicPeak installation.

+ +
+

Installed version: {{current_version}}

+

Channel: {{channel}}

+

Recipient address: {{recipient_email}}

+
+ +

If you can read this email, your SMTP configuration and the recipient +list are working correctly. When a real new version becomes available, +PicPeak will send a separate notification with release notes and update +instructions.

+ +

No action is required. +You may safely delete this message.

+ +

Best regards,
+Your PicPeak Installation

`, + body_text_en: `This is a test email + +You are receiving this message because an administrator clicked +"Send Test Email" on the Update Notifications page of your PicPeak +installation. + +Installed version: {{current_version}} +Channel: {{channel}} +Recipient address: {{recipient_email}} + +If you can read this email, your SMTP configuration and the recipient +list are working correctly. When a real new version becomes available, +PicPeak will send a separate notification with release notes and update +instructions. + +No action is required. You may safely delete this message. + +Best regards, +Your PicPeak Installation`, + body_html_de: ` +

Dies ist eine Test-E-Mail

+ +

Sie erhalten diese Nachricht, weil ein Administrator auf der Seite +"Update-Benachrichtigungen" Ihrer PicPeak-Installation auf +Test-E-Mail senden geklickt hat.

+ +
+

Installierte Version: {{current_version}}

+

Kanal: {{channel}}

+

Empfänger-Adresse: {{recipient_email}}

+
+ +

Wenn Sie diese E-Mail lesen können, funktionieren Ihre SMTP-Konfiguration +und die Empfängerliste korrekt. Sobald eine echte neue Version verfügbar +ist, sendet PicPeak eine separate Benachrichtigung mit Versionshinweisen +und Update-Anweisungen.

+ +

Es ist keine Aktion +erforderlich. Sie können diese Nachricht gefahrlos löschen.

+ +

Mit freundlichen Grüßen,
+Ihre PicPeak-Installation

`, + body_text_de: `Dies ist eine Test-E-Mail + +Sie erhalten diese Nachricht, weil ein Administrator auf der Seite +"Update-Benachrichtigungen" Ihrer PicPeak-Installation auf +"Test-E-Mail senden" geklickt hat. + +Installierte Version: {{current_version}} +Kanal: {{channel}} +Empfänger-Adresse: {{recipient_email}} + +Wenn Sie diese E-Mail lesen können, funktionieren Ihre SMTP-Konfiguration +und die Empfängerliste korrekt. Sobald eine echte neue Version verfügbar +ist, sendet PicPeak eine separate Benachrichtigung mit Versionshinweisen +und Update-Anweisungen. + +Es ist keine Aktion erforderlich. Sie können diese Nachricht gefahrlos löschen. + +Mit freundlichen Grüßen, +Ihre PicPeak-Installation`, + variables: JSON.stringify(['current_version', 'channel', 'recipient_email']) + }); + + console.log('Migration 087_add_update_notification_test_template completed'); +}; + +exports.down = async function(knex) { + console.log('Rollback: 087_add_update_notification_test_template'); + await knex('email_templates') + .where('template_key', 'version_update_test') + .del(); +}; diff --git a/backend/src/routes/adminSystem.js b/backend/src/routes/adminSystem.js index e44d32a0..e72af785 100644 --- a/backend/src/routes/adminSystem.js +++ b/backend/src/routes/adminSystem.js @@ -11,7 +11,7 @@ const { checkForUpdates, getCurrentChannel } = require('../services/updateCheckS const { detectEnvironment, generateUpdateInstructions } = require('../services/environmentService'); const { checkAndNotifyUpdates, - sendUpdateNotificationNow, + sendTestUpdateNotification, getUpdateNotificationSettings } = require('../services/updateNotificationService'); const router = express.Router(); @@ -352,13 +352,18 @@ router.put('/updates/notifications', adminAuth, requirePermission('settings.edit }); // Manually trigger update notification email +// Send a test update notification email. Uses the dedicated +// `version_update_test` template (migration 087) rather than reusing +// `version_update_available`, so admins on the latest version can still +// verify their SMTP + recipient config — the previous handler bailed +// with "No updates available" when nothing was pending (#418). router.post('/updates/notifications/send', adminAuth, requirePermission('settings.edit'), async (req, res) => { try { - const result = await sendUpdateNotificationNow(); + const result = await sendTestUpdateNotification(); res.json(result); } catch (error) { - logger.error('Error sending update notification:', error); - res.status(500).json({ error: 'Failed to send update notification' }); + logger.error('Error sending test update notification:', error); + res.status(500).json({ error: 'Failed to send test update notification' }); } }); diff --git a/backend/src/services/updateNotificationService.js b/backend/src/services/updateNotificationService.js index d1563925..81422107 100644 --- a/backend/src/services/updateNotificationService.js +++ b/backend/src/services/updateNotificationService.js @@ -172,75 +172,82 @@ async function checkAndNotifyUpdates() { } /** - * Force send update notification (for manual trigger from admin UI) + * Send a TEST notification email to the configured recipients (manual + * trigger from the admin "Send Test Email" button on the Update + * Notifications page). + * + * Uses the version_update_test template (migration 087) which is + * explicitly labelled as a configuration check rather than a real update + * notice. Crucially this path does NOT require updateAvailable to be + * true — it sends regardless of whether the instance is on the latest + * version, so admins can verify their SMTP + recipient list work before + * an actual update lands (#418). + * + * Does NOT update last_notified_version — that field is owned by the + * real-update path so a test send doesn't shadow a future genuine + * notification for the same version. */ -async function sendUpdateNotificationNow() { - logger.info('Manually triggering update notification...'); +async function sendTestUpdateNotification() { + logger.info('Sending test update notification email...'); try { - // Check for available updates - const updateInfo = await checkForUpdates(true); // Force refresh - - if (!updateInfo.updateAvailable) { - return { success: false, message: 'No updates available' }; - } - - const newVersion = updateInfo.latest.forChannel; const settings = await getUpdateNotificationSettings(); - // Get recipients const recipients = await getNotificationRecipients(settings.recipients); - if (recipients.length === 0) { return { success: false, message: 'No recipients configured' }; } - // Ensure email transporter is initialized - await initializeTransporter(); + // checkForUpdates is best-effort here — we want the version + channel + // for the email body, but a transient failure shouldn't block the test + // send. Fall back to env-derived defaults so the email still goes out. + let updateInfo; + try { + updateInfo = await checkForUpdates(true); + } catch (error) { + logger.warn('checkForUpdates failed during test send, using fallbacks:', error.message); + updateInfo = { + current: process.env.npm_package_version || 'unknown', + channel: process.env.UPDATE_CHANNEL || 'stable' + }; + } - // Send email to each recipient - const releaseNotesUrl = `https://github.com/the-luap/picpeak/releases/tag/v${newVersion}`; const channelLabel = updateInfo.channel === 'beta' ? 'Beta' : 'Stable'; + await initializeTransporter(); + let successCount = 0; let errorCount = 0; for (const email of recipients) { try { - await sendTemplateEmail(email, 'version_update_available', { + await sendTemplateEmail(email, 'version_update_test', { current_version: updateInfo.current, - new_version: newVersion, channel: channelLabel, - release_notes_url: releaseNotesUrl + recipient_email: email }); successCount++; } catch (error) { errorCount++; - logger.error(`Failed to send update notification to ${email}:`, error); + logger.error(`Failed to send test update notification to ${email}:`, error); } } - // Update last notified version - if (successCount > 0) { - await updateLastNotifiedVersion(newVersion); - } - return { success: successCount > 0, - newVersion, successCount, errorCount, totalRecipients: recipients.length }; } catch (error) { - logger.error('Error sending manual update notification:', error); + logger.error('Error sending test update notification:', error); return { success: false, message: error.message }; } } module.exports = { checkAndNotifyUpdates, - sendUpdateNotificationNow, + sendTestUpdateNotification, getUpdateNotificationSettings, getNotificationRecipients };