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..66a226879 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 { } } + /** + * Generate SVG Status + * @param {response} response express response + * @param {slug} slug from router + * @param {config} config config for the svg + * @returns {Promise} returns a svg file + */ + 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,105 @@ class StatusPage extends BeanModel { return $.root().html(); } + /** + * render SVG file + * @param {slug} slug from router + * @param {userConfig} userConfig generated by user + * @param {statusPage} statusPage object + * @returns {Promise} a SVG string + */ + 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 +294,7 @@ class StatusPage extends BeanModel { } } - if (! hasUp) { + if (!hasUp) { status = STATUS_PAGE_ALL_DOWN; } diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 893f57564..c97b12d44 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..4b70ec512 --- /dev/null +++ b/src/components/BadgeBuilderDialog.vue @@ -0,0 +1,152 @@ + + + + + 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..eecacbf99 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." } diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 116968282..601f9e869 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -19,7 +19,10 @@
- +
{{ $t("markdownSupported") }}
@@ -28,7 +31,10 @@
- +
{{ $t("markdownSupported") }}
@@ -36,7 +42,10 @@
- +
{{ $t("Refresh Interval Description", [config.autoRefreshInterval]) }}
@@ -52,40 +61,63 @@
- +
- +
- - + +
- - + +
  • - -
  • @@ -95,13 +127,19 @@
    - +
    {{ $t("Custom CSS") }}
    - +
    @@ -127,7 +165,7 @@
    -
    +

    @@ -139,14 +177,8 @@ @@ -162,14 +194,22 @@ {{ $t("Edit Status Page") }} - - + + {{ $t("Go to Dashboard") }} + + + + {{ $t("Generate Badge") }} +

    - @@ -177,28 +217,43 @@
    - - + {{ $t("deleteStatusPageMsg") }} @@ -358,6 +447,8 @@ {{ config.customCSS }}
    + +