notifications: send email when manual platform update is required

This commit is contained in:
Girish Ramakrishnan
2026-03-21 15:38:12 +01:00
parent 2d5dc9a6aa
commit cd6acfb91d
9 changed files with 177 additions and 23 deletions
+4
View File
@@ -3205,3 +3205,7 @@
* passkey: implement passwordless login
* oidcserver: fix jwks_rsaonly response
[9.1.6]
* apps: fix wrong disabled state for devices config
* notifications: send email when manual platform update required
+35 -5
View File
@@ -48,7 +48,9 @@
"configure": "Nakonfigurovat",
"restart": "Restartovat",
"reset": "Zresetovat",
"loadMore": "Načíst více"
"loadMore": "Načíst více",
"setup": "Nastavit",
"disable": "Zakázat"
},
"rebootDialog": {
"title": "Restart serveru",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Dvoufaktorová autentizace",
"totpEnabled": "Použít časově omezené jednorázové heslo (TOTP)",
"passkeyEnabled": "Použít passkey"
"totpEnabled": "Povoleno",
"passkeyEnabled": "Povoleno",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "nenastaveno",
"enablePasskey": {
"title": "Povolit passkey"
},
"enableTotp": {
"title": "Povolit TOTP"
},
"disableTotp": {
"title": "Zakázat TOTP"
},
"disablePasskey": {
"title": "Zakázat passkey"
}
},
"backups": {
@@ -648,7 +665,10 @@
"stopUpdateAction": "Zastavit aktualizaci",
"disabled": "Zakázáno",
"onLatest": "poslední",
"description": "Aktualizace platformy a aplikací se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>."
"description": "Aktualizace se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>.",
"config": "Automatické aktualizace",
"platformAndApps": "Platforma a aplikace",
"appsOnly": "Pouze aplikace"
},
"updateScheduleDialog": {
"disableCheckbox": "Zakázat automatické aktualizace",
@@ -673,6 +693,14 @@
"registryConfig": {
"provider": "Poskytovatel docker registrů",
"providerOther": "Jiné"
},
"configureUpdates": {
"title": "Konfigurovat automatické aktualizace",
"policy": "Politika",
"policyDescription": "Vyberte, co se bude automaticky aktualizovat",
"days": "Dnů/y",
"hours": "Hodin/y",
"schedule": "Plány záloh"
}
},
"branding": {
@@ -1594,7 +1622,9 @@
"errorIncorrect2FAToken": "Token 2FA je neplatný",
"errorInternal": "Interní chyba, zkuste akci opakovat později",
"loginAction": "Přihlásit se",
"usePasskeyAction": "Použít passkey"
"usePasskeyAction": "Použít passkey",
"passkeyAction": "Přihlásit se přes passkey",
"errorPasskeyFailed": "Přihlášení pomocí passkey selhalo"
},
"passwordReset": {
"title": "Reset hesla",
+2 -1
View File
@@ -912,7 +912,8 @@
"rebootRequired": "Server reboot required",
"cloudronUpdateFailed": "Cloudron update failed",
"diskSpace": "Low disk space",
"appAutoUpdateFailed": "App automatic update failed"
"appAutoUpdateFailed": "App automatic update failed",
"manualUpdateRequired": "Platform or app requires manual update"
},
"settingsDialog": {
"description": "An email will be sent for the selected events to your primary email."
+38 -7
View File
@@ -44,7 +44,9 @@
"restart": "Mulai ulang",
"reset": "Atur Ulang",
"logs": "Log",
"loadMore": "Muat lebih banyak"
"loadMore": "Muat lebih banyak",
"setup": "Siapkan",
"disable": "Nonaktifkan"
},
"searchPlaceholder": "Cari",
"actions": "Tindakan",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Autentikasi dua faktor",
"totpEnabled": "Menggunakan kata sandi sekali pakai berbasis waktu (TOTP)",
"passkeyEnabled": "Menggunakan passkey"
"totpEnabled": "Diaktifkan",
"passkeyEnabled": "Diaktifkan",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "Belum diatur",
"enablePasskey": {
"title": "Aktifkan passkey"
},
"enableTotp": {
"title": "Aktifkan TOTP"
},
"disableTotp": {
"title": "Nonaktifkan TOTP"
},
"disablePasskey": {
"title": "Nonaktifkan Passkey"
}
},
"backups": {
@@ -724,8 +741,11 @@
"updateAvailableAction": "Pembaruan tersedia",
"stopUpdateAction": "Hentikan pembaruan",
"disabled": "Dinonaktifkan",
"description": "Pembaruan platform dan aplikasi diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
"onLatest": "terbaru"
"description": "Pembaruan diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
"onLatest": "terbaru",
"config": "Pembaruan otomatis",
"appsOnly": "Hanya aplikasi",
"platformAndApps": "Platform & aplikasi"
},
"updateScheduleDialog": {
"disableCheckbox": "Nonaktifkan pembaruan otomatis",
@@ -750,6 +770,14 @@
"registryConfig": {
"provider": "Penyedia registri Docker",
"providerOther": "Lainnya"
},
"configureUpdates": {
"title": "Konfigurasi Pembaruan Otomatis",
"policy": "Kebijakan",
"policyDescription": "Pilih apa yang diperbarui secara otomatis",
"days": "Hari",
"hours": "Jam",
"schedule": "Jadwal"
}
},
"support": {
@@ -1662,7 +1690,8 @@
"appDown": "Aplikasi sedang tidak berfungsi",
"rebootRequired": "Diperlukan menyalakan ulang server",
"cloudronUpdateFailed": "Pembaruan Cloudron gagal",
"diskSpace": "Ruang disk hampir penuh"
"diskSpace": "Ruang disk hampir penuh",
"appAutoUpdateFailed": "Pembaruan otomatis aplikasi gagal"
},
"settingsDialog": {
"description": "E-mail akan dikirimkan ke e-mail utama Anda untuk acara-acara yang dipilih."
@@ -1688,7 +1717,9 @@
"errorIncorrect2FAToken": "Token 2FA tidak valid",
"errorInternal": "Terjadi kesalahan internal, coba lagi nanti",
"loginAction": "Masuk",
"usePasskeyAction": "Gunakan passkey"
"usePasskeyAction": "Gunakan passkey",
"errorPasskeyFailed": "Gagal masuk dengan passkey",
"passkeyAction": "Masuk dengan passkey"
},
"passwordReset": {
"title": "Pengaturan ulang kata sandi",
+38 -7
View File
@@ -47,7 +47,9 @@
"configure": "Configureer",
"restart": "Herstart",
"reset": "Reset",
"loadMore": "Laad meer"
"loadMore": "Laad meer",
"setup": "Instellen",
"disable": "Uitschakelen"
},
"rebootDialog": {
"title": "Herstart Server",
@@ -361,8 +363,23 @@
},
"twoFactorAuth": {
"title": "Twee-Factor (2FA) authenticatie",
"totpEnabled": "Gebruikt tijdgebaseerd eenmalige wachtwoord (TOTP)",
"passkeyEnabled": "Gebruikt passkey"
"totpEnabled": "Ingeschakeld",
"passkeyEnabled": "Ingeschakeld",
"totpTitle": "TOTP",
"passkeyTitle": "Passkey"
},
"notSet": "Niet ingesteld",
"enablePasskey": {
"title": "Passkey activeren"
},
"enableTotp": {
"title": "TOTP activeren"
},
"disableTotp": {
"title": "TOTP Uitschakelen"
},
"disablePasskey": {
"title": "Passkey uitschakelen"
}
},
"backups": {
@@ -1141,9 +1158,12 @@
"checkForUpdatesAction": "Controleer op updates",
"updateAvailableAction": "Update beschikbaar",
"stopUpdateAction": "Stop update",
"description": "Platform en app updates worden toegepast met de geconfigureerde planning met deze <a href=\"/#/system-locale\">Systeem tijdzone</a>.",
"description": "Updates worden toegepast volgens het geconfigureerde schema, met behulp van de <a href=\"/#/system-settings\">System time zone</a>.",
"disabled": "Uitgeschakeld",
"onLatest": "Laatste"
"onLatest": "Laatste",
"config": "Automatische updates",
"appsOnly": "Alleen Apps",
"platformAndApps": "Platform & Apps"
},
"updateScheduleDialog": {
"disableCheckbox": "Automatische updates uitschakelen",
@@ -1169,6 +1189,14 @@
"registryConfig": {
"provider": "Docker registry aanbieder",
"providerOther": "Anders"
},
"configureUpdates": {
"title": "Automatische updates configureren",
"policy": "Beleid",
"policyDescription": "Kies wat er automatisch wordt bijgewerkt",
"days": "Dagen",
"hours": "Uren",
"schedule": "Planning"
}
},
"support": {
@@ -1226,7 +1254,8 @@
"appDown": "App werkt niet",
"rebootRequired": "Server herstart noodzakelijk",
"cloudronUpdateFailed": "Cloudron update mislukt",
"diskSpace": "Weinig diskruimte"
"diskSpace": "Weinig diskruimte",
"appAutoUpdateFailed": "Automatische update van de app is mislukt"
},
"settingsDialog": {
"description": "Een e-mail wordt verstuurd voor de geselecteerde gebeurtenissen naar je primaire e-mail."
@@ -1513,7 +1542,9 @@
"errorIncorrect2FAToken": "2FA token is niet geldig",
"errorInternal": "Interne fout, probeer later opnieuw",
"loginAction": "Inloggen",
"usePasskeyAction": "Gebruik een passkey"
"usePasskeyAction": "Gebruik een passkey",
"errorPasskeyFailed": "Inloggen met passkey mislukt",
"passkeyAction": "Inloggen met een passkey"
},
"passwordReset": {
"title": "Wachtwoord herstellen",
@@ -17,6 +17,7 @@ const appAutoUpdateFailed = ref(false);
const certificateRenewalFailed = ref(false);
const diskSpace = ref(false);
const cloudronUpdateFailed = ref(false);
const manualUpdateRequired = ref(false);
const reboot = ref(false);
async function onSubmit() {
@@ -31,6 +32,7 @@ async function onSubmit() {
if (certificateRenewalFailed.value) config.push('certificateRenewalFailed');
if (diskSpace.value) config.push('diskSpace');
if (cloudronUpdateFailed.value) config.push('cloudronUpdateFailed');
if (manualUpdateRequired.value) config.push('manualUpdateRequired');
if (reboot.value) config.push('reboot');
const [error] = await profileModel.setNotificationConfig(config);
@@ -55,6 +57,7 @@ async function open() {
certificateRenewalFailed.value = config.indexOf('certificateRenewalFailed') !== -1;
diskSpace.value = config.indexOf('diskSpace') !== -1;
cloudronUpdateFailed.value = config.indexOf('cloudronUpdateFailed') !== -1;
manualUpdateRequired.value = config.indexOf('manualUpdateRequired') !== -1;
reboot.value = config.indexOf('reboot') !== -1;
dialogItem.value.open();
@@ -121,6 +124,11 @@ defineExpose({
<Switch v-model="cloudronUpdateFailed" :disabled="busy"/>
</SettingsItem>
<SettingsItem>
<div>{{ $t('notifications.settings.manualUpdateRequired') }}</div>
<Switch v-model="manualUpdateRequired" :disabled="busy"/>
</SettingsItem>
<SettingsItem>
<div>{{ $t('notifications.settings.rebootRequired') }}</div>
<Switch v-model="reboot" :disabled="busy"/>
@@ -0,0 +1,14 @@
Dear <%= cloudronName %> Admin,
Cloudron v<%= version %> is available.
Changelog:
<%- changelog %>
Go to the Updates view to update: <%= updateUrl %>
Powered by https://cloudron.io
Don't want such mails? Change your notification preferences at <%= notificationsUrl %>
Sent at: <%= new Date().toUTCString() %>
+18
View File
@@ -322,6 +322,23 @@ async function boxUpdateError(mailTo, message) {
await sendMail(mailOptions);
}
async function boxManualUpdateRequired(mailTo, version, changelog) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof changelog, 'string');
const mailConfig = await getMailConfig();
const mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Cloudron v${version} is available`,
text: render('box_manual_update_required-text.ejs', { cloudronName: mailConfig.cloudronName, version, changelog, updateUrl: `https://${mailConfig.dashboardFqdn}/#/system-update`, notificationsUrl: mailConfig.notificationsUrl })
};
await sendMail(mailOptions);
}
async function certificateRenewalError(mailTo, domain, message) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -368,6 +385,7 @@ export default {
appUp,
oomEvent,
rebootRequired,
boxManualUpdateRequired,
boxUpdateError,
lowDiskSpace,
sendTestMail,
+20 -3
View File
@@ -29,6 +29,7 @@ const TYPE_UPDATE_UBUNTU = 'ubuntuUpdate';
const TYPE_BOX_UPDATE = 'boxUpdate';
const TYPE_MANUAL_APP_UPDATE_NEEDED = 'manualAppUpdate';
const TYPE_APP_AUTO_UPDATE_FAILED = 'appAutoUpdateFailed';
const TYPE_MANUAL_UPDATE_REQUIRED = 'manualUpdateRequired';
const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'type', 'title', 'message', 'creationTime', 'acknowledged', 'context' ];
@@ -298,15 +299,30 @@ async function lowDiskSpace(message) {
}
}
async function onPin(type, message) {
async function boxManualUpdateRequired(version, changelogText) {
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof changelogText, 'string');
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(TYPE_MANUAL_UPDATE_REQUIRED)) {
await safe(mailer.boxManualUpdateRequired(admin.email, version, changelogText), { debug: log });
}
}
}
async function onPin(type, message, options) {
assert.strictEqual(typeof type, 'string'); // TYPE_
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof options, 'object');
switch (type) {
case TYPE_REBOOT:
return await rebootRequired();
case TYPE_DISK_SPACE:
return await lowDiskSpace(message);
case TYPE_BOX_UPDATE:
return await boxManualUpdateRequired(options.context, message);
default:
break;
}
@@ -320,7 +336,7 @@ async function pin(type, title, message, options) {
const result = await getByType(type, options.context || '');
if (!result) {
await onPin(type, message);
await onPin(type, message, options);
return await add(type, title, message, { eventId: null, context: options.context || '' });
}
@@ -328,7 +344,7 @@ async function pin(type, title, message, options) {
const isUpdateType = type === TYPE_BOX_UPDATE || type === TYPE_MANUAL_APP_UPDATE_NEEDED;
const acknowledged = (isUpdateType && result.message === message) ? result.acknowledged : false;
if (result.acknowledged && !acknowledged) await onPin(type, message);
if (result.acknowledged && !acknowledged) await onPin(type, message, options);
await update(result, { title, message, acknowledged, creationTime: new Date() });
return result.id;
@@ -412,6 +428,7 @@ export default {
TYPE_BOX_UPDATE,
TYPE_MANUAL_APP_UPDATE_NEEDED,
TYPE_APP_AUTO_UPDATE_FAILED,
TYPE_MANUAL_UPDATE_REQUIRED,
TYPE_DOMAIN_CONFIG_CHECK_FAILED,
pin,
unpin,