diff --git a/package-lock.json b/package-lock.json index 8d3f58b4d..ccf1a628c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", diff --git a/server/model/status_page.js b/server/model/status_page.js index 38f548ebb..08a69154c 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -9,6 +9,7 @@ const { Feed } = require("feed"); const config = require("../config"); const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util"); +const { makeBadge } = require("badge-maker"); class StatusPage extends BeanModel { @@ -16,7 +17,7 @@ class StatusPage extends BeanModel { * Like this: { "test-uptime.kuma.pet": "default" } * @type {{}} */ - static domainMappingList = { }; + static domainMappingList = {}; /** * Handle responses to RSS pages @@ -61,6 +62,26 @@ class StatusPage extends BeanModel { } } + /** + * Handle responses to status page + * @param {Response} response Response object + * @param {string} slug Status page slug + * @param {object} config Config for badge + * @returns {Promise} + */ + static async handleStatusPageSVGResponse(response, slug, config) { + slug = slug.replace('.svg', '') + let statusPage = await R.findOne("status_page", " slug = ? ", [ + slug.replace('.svg', '') + ]); + if (statusPage) { + response.set('Content-Type', 'image/svg+xml') + response.send(await StatusPage.renderSVG(slug, config, statusPage)); + } else { + response.status(404).send(await StatusPage.renderSVG(slug, config, null)); + } + } + /** * SSR for RSS feed * @param {statusPage} statusPage object @@ -152,6 +173,62 @@ class StatusPage extends BeanModel { return $.root().html(); } + static async renderSVG(slug, userConfig = {}, statusPage) { + let allowedBadgeStyles = ['flat', 'flat-square', 'for-the-badge', 'plastic', 'social']; + if (!allowedBadgeStyles.includes(userConfig.badgeStyle)) { + userConfig.badgeStyle = 'flat' + } + + // returns unknown svg + if (statusPage === null) { + return makeBadge({ message: userConfig.notFoundText || 'not found', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }) + } else { + + // get all heartbeats that correspond to this statusPage + const config = await statusPage.toPublicJSON(); + + // Public Group List + const showTags = !!statusPage.show_tags; + + const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [ + statusPage.id + ]); + + let heartbeats = []; + + for (let groupBean of list) { + let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); + for (const monitor of monitorGroup.monitorList) { + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); + if (heartbeat) { + heartbeats.push({ + ...monitor, + status: heartbeat.status, + time: heartbeat.time + }); + } + } + } + + // calculate RSS feed description + let status = StatusPage.overallStatus(heartbeats); + + switch (status) { + case STATUS_PAGE_ALL_DOWN: + return makeBadge({ message: userConfig.allDownText ?? 'all down', color: 'red', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_ALL_UP: + return makeBadge({ message: userConfig.allOperationalText ?? 'all operational', color: 'green', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_PARTIAL_DOWN: + return makeBadge({ message: userConfig.partialDownText ?? 'partial down', color: 'orange', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + case STATUS_PAGE_MAINTENANCE: + return makeBadge({ message: userConfig.maintenanceText || 'maintenance', color: 'yellow', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + default: + return makeBadge({ message: userConfig.unknownText || 'unknown', color: 'lightgray', style: userConfig.badgeStyle, label: slug, labelColor: 'gray' }); + } + } + + } + /** * @param {heartbeats} heartbeats from getRSSPageData * @returns {number} status_page constant from util.ts @@ -174,7 +251,7 @@ class StatusPage extends BeanModel { } } - if (! hasUp) { + if (!hasUp) { status = STATUS_PAGE_ALL_DOWN; } @@ -231,7 +308,7 @@ class StatusPage extends BeanModel { for (let groupBean of list) { let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry); for (const monitor of monitorGroup.monitorList) { - const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]); + const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [monitor.id]); if (heartbeat) { heartbeats.push({ ...monitor, @@ -471,7 +548,7 @@ class StatusPage extends BeanModel { SELECT DISTINCT maintenance_id FROM maintenance_status_page WHERE status_page_id = ? - `, [ statusPageId ]); + `, [statusPageId]); for (const maintenanceID of maintenanceIDList) { let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 893f57564..9c5b88101 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -16,7 +16,13 @@ const server = UptimeKumaServer.getInstance(); router.get("/status/:slug", cache("5 minutes"), async (request, response) => { let slug = request.params.slug; slug = slug.toLowerCase(); - await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); + + if (slug.endsWith('.svg')) { + await StatusPage.handleStatusPageSVGResponse(response, slug, JSON.parse(request.query.config || '{}')); + } else { + await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); + } + }); router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => { diff --git a/src/components/BadgeBuilderDialog.vue b/src/components/BadgeBuilderDialog.vue new file mode 100644 index 000000000..8b4f9cc79 --- /dev/null +++ b/src/components/BadgeBuilderDialog.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/icon.js b/src/icon.js index 7bdfe1ca0..4bfb279ea 100644 --- a/src/icon.js +++ b/src/icon.js @@ -18,6 +18,7 @@ import { faPlus, faSearch, faTachometerAlt, + faTag, faTimes, faTimesCircle, faTrash, @@ -64,6 +65,7 @@ library.add( faPlus, faSearch, faTachometerAlt, + faTag, faTimes, faTimesCircle, faTrash, diff --git a/src/lang/en.json b/src/lang/en.json index e215f1031..711d8b1eb 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1051,5 +1051,20 @@ "RabbitMQ Password": "RabbitMQ Password", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "SendGrid API Key": "SendGrid API Key", - "Separate multiple email addresses with commas": "Separate multiple email addresses with commas" -} + "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", + "Generate Badge": "Generate Badge", + "Return to not found": "Return to not found", + "Return to operational": "Return to operational", + "Return to partial down": "Return to partial down", + "Return to maintenance": "Return to maintenance", + "Return to unknown": "Return to unknown", + "not found": "not found", + "operational": "operational", + "partial down": "partial down", + "maintenance": "maintenance", + "unknown": "unknown", + "Preview": "Preview", + "Copy Image URL": "Copy Image URL", + "Image url copied to clipboard.": "Image url copied to clipboard.", + "Unable to copy image URL.": "Unable to copy image URL." +} \ No newline at end of file diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 8319c656d..8d8682502 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -994,5 +994,20 @@ "aboutSlackUsername": "Altera o nome de exibição do remetente da mensagem. Se quiser mencionar alguém, inclua a menção no nome amigável.", "Send rich messages": "Enviar mensagens ricas", "Host Onesender": "Servidor Onesender", - "Token Onesender": "Chave Onesender" -} + "Token Onesender": "Chave Onesender", + "Generate Badge": "Gerar Distintivo", + "Return to not found": "Retorno para não encontrado", + "Return to operational": "Retorno para operacional", + "Return to partial down": "Retorno para parcialmente degradado", + "Return to maintenance": "Retorno para em manutenção", + "Return to unknown": "Retorno para desconhecido", + "not found": "não encontrado", + "operational": "operacional", + "partial down": "parcialmente fora", + "maintenance": "manutenção", + "unknown": "desconhecido", + "Preview": "Pré-visualizar", + "Copy Image URL": "Copiar URL da imagem", + "Image url copied to clipboard.": "URL da imagem copiada para área de transferência.", + "Unable to copy image URL.": "Não é possível copiar o URL da imagem para área de transferência." +} \ No newline at end of file diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 116968282..b8d585f39 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -19,7 +19,8 @@
- +
{{ $t("markdownSupported") }}
@@ -28,7 +29,8 @@
- +
{{ $t("markdownSupported") }}
@@ -36,7 +38,8 @@
- +
{{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
@@ -52,40 +55,49 @@
- +
- +
- - + +
- - + +
  • - -
  • @@ -95,13 +107,15 @@
    - +
    {{ $t("Custom CSS") }}
    - +
    @@ -127,7 +141,7 @@
    -
    +

    @@ -138,17 +152,9 @@ - + @@ -162,14 +168,20 @@ {{ $t("Edit Status Page") }} - - + + {{ $t("Go to Dashboard") }} + + + + {{ $t("Generate Badge") }} +

    - @@ -177,28 +189,35 @@
    -