diff --git a/migrations/20230922150232-notifications-add-type.js b/migrations/20230922150232-notifications-add-type.js new file mode 100644 index 000000000..9c56ce3d0 --- /dev/null +++ b/migrations/20230922150232-notifications-add-type.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE notifications ADD COLUMN type VARCHAR(128) NOT NULL DEFAULT ""'); +}; + +exports.down = async function (db) { + await db.runSql('ALTER TABLE notifications DROP COLUMN type'); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 31c3daa25..465717b16 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -254,6 +254,7 @@ CREATE TABLE IF NOT EXISTS tasks( CREATE TABLE IF NOT EXISTS notifications( id int NOT NULL AUTO_INCREMENT, eventId VARCHAR(128), // reference to eventlog. can be null + type VARCHAR(128) NOT NULL DEFAULT "" title VARCHAR(512) NOT NULL, message TEXT, acknowledged BOOLEAN DEFAULT false, diff --git a/src/notifications.js b/src/notifications.js index f41ec551d..cbb5a6515 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -8,6 +8,11 @@ exports = module.exports = { onEvent, + // these are notification types + ALERT_CLOUDRON_INSTALLED: 'cloudronInstalled', + ALERT_CLOUDRON_UPDATED: 'cloudronUpdated', + ALERT_CLOUDRON_UPDATE_FAILED: 'cloudronUpdateFailed', + ALERT_CERTIFICATE_RENEWAL_FAILED: 'certificateRenewalFailed', ALERT_BACKUP_CONFIG: 'backupConfig', ALERT_DISK_SPACE: 'diskSpace', ALERT_MAIL_STATUS: 'mailStatus', @@ -15,6 +20,9 @@ exports = module.exports = { ALERT_BOX_UPDATE: 'boxUpdate', ALERT_UPDATE_UBUNTU: 'ubuntuUpdate', ALERT_MANUAL_APP_UPDATE: 'manualAppUpdate', + ALERT_APP_OOM: 'appOutOfMemory', + ALERT_APP_UPDATED: 'appUpdated', + ALERT_BACKUP_FAILED: 'backupFailed', alert, clearAlert, @@ -34,7 +42,7 @@ const assert = require('assert'), mailer = require('./mailer.js'), users = require('./users.js'); -const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ]; +const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'type', 'title', 'message', 'creationTime', 'acknowledged' ]; function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -43,13 +51,14 @@ function postProcess(result) { return result; } -async function add(title, message, data) { +async function add(type, title, message, data) { + assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof title, 'string'); assert.strictEqual(typeof message, 'string'); assert.strictEqual(typeof data, 'object'); - const query = 'INSERT INTO notifications (title, message, acknowledged, eventId) VALUES (?, ?, ?, ?)'; - const args = [ title, message, false, data?.eventId || null ]; + const query = 'INSERT INTO notifications (type, title, message, acknowledged, eventId) VALUES (?, ?, ?, ?, ?)'; + const args = [ type, title, message, false, data?.eventId || null ]; const result = await database.query(query, args); return String(result.insertId); @@ -143,7 +152,7 @@ async function oomEvent(eventId, containerId, app, addonName /*, event*/) { message = `The app has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://${dashboardFqdn}/#/app/${app.id}/resources)`; } - await add(title, message, { eventId }); + await add(exports.ALERT_APP_OOM, title, message, { eventId }); } async function appUpdated(eventId, app, fromManifest, toManifest) { @@ -159,7 +168,7 @@ async function appUpdated(eventId, app, fromManifest, toManifest) { const title = upstreamVersion ? `${toManifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${toManifest.version})` : `${toManifest.title} at ${app.fqdn} updated to package version ${toManifest.version}`; - await add(title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${toManifest.changelog}\n`, { eventId }); + await add(exports.ALERT_APP_UPDATED, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${toManifest.changelog}\n`, { eventId }); } async function boxInstalled(eventId, version) { @@ -169,7 +178,7 @@ async function boxInstalled(eventId, version) { const changes = changelog.getChanges(version.replace(/\.([^.]*)$/, '.0')); // last .0 release const changelogMarkdown = changes.map((m) => `* ${m}\n`).join(''); - await add(`Cloudron v${version} installed`, `Cloudron v${version} was installed.\n\nPlease join our community at ${constants.FORUM_URL} .\n\nChangelog:\n${changelogMarkdown}\n`, { eventId }); + await add(exports.ALERT_CLOUDRON_INSTALLED, `Cloudron v${version} installed`, `Cloudron v${version} was installed.\n\nPlease join our community at ${constants.FORUM_URL} .\n\nChangelog:\n${changelogMarkdown}\n`, { eventId }); } async function boxUpdated(eventId, oldVersion, newVersion) { @@ -180,14 +189,14 @@ async function boxUpdated(eventId, oldVersion, newVersion) { const changes = changelog.getChanges(newVersion); const changelogMarkdown = changes.map((m) => `* ${m}\n`).join(''); - await add(`Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId }); + await add(exports.ALERT_CLOUDRON_UPDATED, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId }); } async function boxUpdateError(eventId, errorMessage) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - await add('Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, { eventId }); + await add(exports.ALERT_CLOUDRON_UPDATE_FAILED, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, { eventId }); } async function certificateRenewalError(eventId, fqdn, errorMessage) { @@ -195,7 +204,7 @@ async function certificateRenewalError(eventId, fqdn, errorMessage) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - await add(`Certificate renewal of ${fqdn} failed`, `Failed to renew certs of ${fqdn}: ${errorMessage}. Renewal will be retried in 12 hours.`, { eventId }); + await add(exports.ALERT_CERTIFICATE_RENEWAL_FAILED, `Certificate renewal of ${fqdn} failed`, `Failed to renew certs of ${fqdn}: ${errorMessage}. Renewal will be retried in 12 hours.`, { eventId }); const admins = await users.getAdmins(); for (const admin of admins) { @@ -208,7 +217,7 @@ async function backupFailed(eventId, taskId, errorMessage) { assert.strictEqual(typeof taskId, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - await add('Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/frontend/logs.html?taskId=${taskId}).`, { eventId }); + await add(exports.ALERT_BACKUP_FAILED, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/frontend/logs.html?taskId=${taskId}).`, { eventId }); // only send mail if the past 3 automated backups failed const backupEvents = await eventlog.listPaged([eventlog.ACTION_BACKUP_FINISH], null /* search */, 1, 20); @@ -226,16 +235,16 @@ async function backupFailed(eventId, taskId, errorMessage) { } } -// id is unused but nice to search code -async function alert(id, title, message, options) { - assert.strictEqual(typeof id, 'string'); +// type must be one of ALERT_* +async function alert(type, title, message, options) { + assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof title, 'string'); assert.strictEqual(typeof message, 'string'); assert.strictEqual(typeof options, 'object'); const result = await getByTitle(title); - if (!result) return await add(title, message, { eventId: null }); + if (!result) return await add(type, title, message, { eventId: null }); if (!options.persist) return result.id; await update(result, { @@ -250,11 +259,10 @@ async function alert(id, title, message, options) { } // id is unused but nice to search code -async function clearAlert(id, title) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof title, 'string'); +async function clearAlert(type) { + assert.strictEqual(typeof type, 'string'); - await database.query('DELETE FROM notifications WHERE title = ?', [ title ]); + await database.query('DELETE FROM notifications WHERE type = ?', [ type ]); return null; } diff --git a/src/routes/test/notifications-test.js b/src/routes/test/notifications-test.js index 0923e9698..5fc0bf967 100644 --- a/src/routes/test/notifications-test.js +++ b/src/routes/test/notifications-test.js @@ -19,7 +19,7 @@ describe('Notifications API', function () { it('can add notifications', async function () { for (let i = 0; i < 3; i++) { - const id = await notifications._add(`title ${i}`, `message ${i}`, { eventId: null }); + const id = await notifications._add(notifications.ALERT_APP_UPDATED, `title ${i}`, `message ${i}`, { eventId: null }); notificationIds.push(id); } }); diff --git a/src/test/notifications-test.js b/src/test/notifications-test.js index 8c5935aec..f3e98500a 100644 --- a/src/test/notifications-test.js +++ b/src/test/notifications-test.js @@ -30,7 +30,7 @@ describe('Notifications', function () { it('can add notifications', async function () { for (let i = 0; i < 3; i++) { - const [error, id] = await safe(notifications._add(`title ${i}`, `message ${i}`, { eventId: EVENT_0.id })); + const [error, id] = await safe(notifications._add(notifications.ALERT_APP_UPDATED, `title ${i}`, `message ${i}`, { eventId: EVENT_0.id })); expect(error).to.equal(null); expect(id).to.be.a('string'); notificationIds.push(id); @@ -42,6 +42,7 @@ describe('Notifications', function () { const [error, result] = await safe(notifications.get(notificationIds[0])); expect(error).to.be(null); expect(result.title).to.be('title 0'); + expect(result.type).to.be(notifications.ALERT_APP_UPDATED); expect(result.message).to.be('message 0'); expect(result.acknowledged).to.be(false); }); @@ -60,10 +61,11 @@ describe('Notifications', function () { }); it('can update notification', async function () { - await notifications.update({ id: notificationIds[0] }, { title: 'updated title 0', message: 'updated message 0', acknowledged: true }); + await notifications.update({ id: notificationIds[0] }, { type: notifications.ALERT_APP_OOM, title: 'updated title 0', message: 'updated message 0', acknowledged: true }); const result = await notifications.get(notificationIds[0]); expect(result.title).to.be('updated title 0'); + expect(result.type).to.be(notifications.ALERT_APP_OOM); expect(result.message).to.be('updated message 0'); expect(result.acknowledged).to.be(true); }); @@ -118,7 +120,7 @@ describe('Notifications', function () { }); it('can clear the alert', async function () { - const id = await notifications.clearAlert(notifications.ALERT_BOX_UPDATE, 'Cloudron xx is available'); + const id = await notifications.clearAlert(notifications.ALERT_BOX_UPDATE); expect(id).to.be(null); const result = await notifications.get(alertId);