Files
picpeak/backend/src/services/updateNotificationService.js
T
Paul Nothaft 50c09904a9 feat: add update instructions dialog, email notifications, and capture date sorting
- Add Update Instructions Dialog with environment-specific commands (Docker/Git/Standalone)
- Add email notification settings for new version alerts
- Add "Sort by Capture Date" option using EXIF metadata extraction
- Fix E2E tests by loading environment variables via dotenv
- Add test-images/ and backend/*.db to .gitignore

Closes #181
2026-02-16 16:23:57 +01:00

247 lines
7.2 KiB
JavaScript

/**
* Update Notification Service
* Checks for updates and sends email notifications to administrators.
*/
const { db } = require('../database/db');
const { checkForUpdates } = require('./updateCheckService');
const { sendTemplateEmail, initializeTransporter } = require('./emailProcessor');
const logger = require('../utils/logger');
/**
* Get update notification settings from database
*/
async function getUpdateNotificationSettings() {
try {
const settings = await db('app_settings')
.whereIn('setting_key', [
'update_email_notifications_enabled',
'update_email_recipients',
'last_notified_version'
])
.select('setting_key', 'setting_value');
const result = {};
for (const setting of settings) {
try {
result[setting.setting_key] = JSON.parse(setting.setting_value);
} catch (e) {
result[setting.setting_key] = setting.setting_value;
}
}
return {
enabled: result.update_email_notifications_enabled === true,
recipients: result.update_email_recipients || '',
lastNotifiedVersion: result.last_notified_version || ''
};
} catch (error) {
logger.error('Error fetching update notification settings:', error);
return {
enabled: false,
recipients: '',
lastNotifiedVersion: ''
};
}
}
/**
* Update the last notified version in database
*/
async function updateLastNotifiedVersion(version) {
try {
await db('app_settings')
.where('setting_key', 'last_notified_version')
.update({
setting_value: JSON.stringify(version),
updated_at: db.fn.now()
});
} catch (error) {
logger.error('Error updating last notified version:', error);
}
}
/**
* Get admin email addresses to notify
* If recipients setting is empty, get all active admin emails
*/
async function getNotificationRecipients(recipientsSetting) {
try {
if (recipientsSetting && recipientsSetting.trim()) {
// Use configured recipients (comma-separated)
return recipientsSetting.split(',').map(email => email.trim()).filter(Boolean);
}
// Fallback: get all active admin user emails
const admins = await db('admin_users')
.where('is_active', true)
.whereNotNull('email')
.select('email');
return admins.map(admin => admin.email).filter(Boolean);
} catch (error) {
logger.error('Error fetching notification recipients:', error);
return [];
}
}
/**
* Check for updates and send notification emails if new version is available
*/
async function checkAndNotifyUpdates() {
logger.info('Update notification service: Checking for updates...');
try {
// Check if update notifications are enabled
const settings = await getUpdateNotificationSettings();
if (!settings.enabled) {
logger.info('Update email notifications are disabled');
return { notified: false, reason: 'notifications_disabled' };
}
// Check for available updates
const updateInfo = await checkForUpdates();
if (!updateInfo.updateAvailable) {
logger.info('No updates available');
return { notified: false, reason: 'no_updates' };
}
const newVersion = updateInfo.latest.forChannel;
// Check if we've already notified about this version
if (settings.lastNotifiedVersion === newVersion) {
logger.info(`Already notified about version ${newVersion}`);
return { notified: false, reason: 'already_notified' };
}
// Get recipients
const recipients = await getNotificationRecipients(settings.recipients);
if (recipients.length === 0) {
logger.warn('No recipients configured for update notifications');
return { notified: false, reason: 'no_recipients' };
}
// Ensure email transporter is initialized
await initializeTransporter();
// Send email to each recipient
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
const releaseNotesUrl = `https://github.com/the-luap/picpeak/releases/tag/v${newVersion}`;
const channelLabel = updateInfo.channel === 'beta' ? 'Beta' : 'Stable';
let successCount = 0;
let errorCount = 0;
for (const email of recipients) {
try {
await sendTemplateEmail(email, 'version_update_available', {
current_version: updateInfo.current,
new_version: newVersion,
channel: channelLabel,
release_notes_url: releaseNotesUrl,
admin_url: `${frontendUrl}/admin`
});
successCount++;
logger.info(`Update notification sent to ${email}`);
} catch (error) {
errorCount++;
logger.error(`Failed to send update notification to ${email}:`, error);
}
}
// Update last notified version
if (successCount > 0) {
await updateLastNotifiedVersion(newVersion);
logger.info(`Update notifications sent: ${successCount} success, ${errorCount} failed`);
}
return {
notified: successCount > 0,
newVersion,
successCount,
errorCount,
totalRecipients: recipients.length
};
} catch (error) {
logger.error('Error in update notification service:', error);
return { notified: false, reason: 'error', error: error.message };
}
}
/**
* Force send update notification (for manual trigger from admin UI)
*/
async function sendUpdateNotificationNow() {
logger.info('Manually triggering update notification...');
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();
// Send email to each recipient
const releaseNotesUrl = `https://github.com/the-luap/picpeak/releases/tag/v${newVersion}`;
const channelLabel = updateInfo.channel === 'beta' ? 'Beta' : 'Stable';
let successCount = 0;
let errorCount = 0;
for (const email of recipients) {
try {
await sendTemplateEmail(email, 'version_update_available', {
current_version: updateInfo.current,
new_version: newVersion,
channel: channelLabel,
release_notes_url: releaseNotesUrl
});
successCount++;
} catch (error) {
errorCount++;
logger.error(`Failed to send 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);
return { success: false, message: error.message };
}
}
module.exports = {
checkAndNotifyUpdates,
sendUpdateNotificationNow,
getUpdateNotificationSettings,
getNotificationRecipients
};