diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml
index 273b1dba2..944627127 100644
--- a/.github/workflows/auto-test.yml
+++ b/.github/workflows/auto-test.yml
@@ -6,8 +6,12 @@ name: Auto Test
on:
push:
branches: [ master ]
+ paths-ignore:
+ - '*.md'
pull_request:
branches: [ master ]
+ paths-ignore:
+ - '*.md'
jobs:
auto-test:
@@ -36,6 +40,7 @@ jobs:
env:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
+
check-linters:
runs-on: ubuntu-latest
diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml
index 026022dfa..762bc9688 100644
--- a/.github/workflows/close-incorrect-issue.yml
+++ b/.github/workflows/close-incorrect-issue.yml
@@ -1,4 +1,3 @@
-
name: Close Incorrect Issue
on:
@@ -12,13 +11,13 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- node-version: [16.x]
+ node-version: [16]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml
index 5b4568e1d..b39f68fc1 100644
--- a/.github/workflows/stale-bot.yml
+++ b/.github/workflows/stale-bot.yml
@@ -3,13 +3,13 @@ on:
workflow_dispatch:
schedule:
- cron: '0 */6 * * *'
-#Run every 6 hours
+#Run every 6 hours
jobs:
stale:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v5
+ - uses: actions/stale@v7
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
diff --git a/README.md b/README.md
index f29622a6e..cdefe6a0c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Uptime Kuma
-[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam)
+[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam)
@@ -18,7 +18,6 @@ Uptime Kuma is an easy-to-use self-hosted monitoring tool.
Try it!
- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
-- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js
index ebc67da31..ada2aca81 100644
--- a/extra/mark-as-nightly.js
+++ b/extra/mark-as-nightly.js
@@ -1,11 +1,12 @@
const pkg = require("../package.json");
const fs = require("fs");
const util = require("../src/util");
+const dayjs = require("dayjs");
util.polyfill();
const oldVersion = pkg.version;
-const newVersion = oldVersion + "-nightly-" + util.genSecret(8);
+const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss");
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);
diff --git a/package-lock.json b/package-lock.json
index c5429446f..fec23fe9c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -89,10 +89,12 @@
"cypress": "^10.1.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
+ "dompurify": "~2.4.3",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
"jest": "~27.2.5",
+ "marked": "~4.2.5",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4",
@@ -7801,6 +7803,12 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
+ "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==",
+ "dev": true
+ },
"node_modules/domutils": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
@@ -13622,6 +13630,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/marked": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz",
+ "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==",
+ "dev": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/mathml-tag-names": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
@@ -24880,6 +24900,12 @@
"domelementtype": "^2.3.0"
}
},
+ "dompurify": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
+ "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==",
+ "dev": true
+ },
"domutils": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
@@ -29114,6 +29140,12 @@
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"dev": true
},
+ "marked": {
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz",
+ "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==",
+ "dev": true
+ },
"mathml-tag-names": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
diff --git a/package.json b/package.json
index 901408363..d5d1c5fdd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
- "version": "1.19.6",
+ "version": "1.20.0-beta.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -146,9 +146,11 @@
"cypress": "^10.1.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
+ "dompurify": "~2.4.3",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
+ "marked": "~4.2.5",
"jest": "~27.2.5",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
diff --git a/server/config.js b/server/config.js
index 0523e7078..43a40f672 100644
--- a/server/config.js
+++ b/server/config.js
@@ -4,6 +4,7 @@ const demoMode = args["demo"] || false;
const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
+ defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
@@ -13,6 +14,11 @@ const badgeConstants = {
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
+ defaultCertExpValueSuffix: " days",
+ defaultCertExpLabelSuffix: "h",
+ // Values Come From Default Notification Times
+ defaultCertExpireWarnDays: "14",
+ defaultCertExpireDownDays: "7"
};
module.exports = {
diff --git a/server/model/monitor.js b/server/model/monitor.js
index c3e91f935..4cbb56e1a 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -1255,7 +1255,7 @@ class Monitor extends BeanModel {
*/
static async getPreviousHeartbeat(monitorID) {
return await R.getRow(`
- SELECT status, time FROM heartbeat
+ SELECT ping, status, time FROM heartbeat
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
`, [
monitorID
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index e95fd045e..665163aee 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -145,7 +145,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
const state = overrideValue !== undefined ? overrideValue : heartbeat.status;
- badgeValues.label = label ?? "";
+ badgeValues.label = label ?? "Status";
switch (state) {
case DOWN:
badgeValues.color = downColor;
@@ -212,7 +212,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
const badgeValues = { style };
if (!publicMonitor) {
- // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
+ // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
@@ -228,8 +228,11 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
badgeValues.color = color ?? percentageToColor(uptime);
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
- // build a lable string. If a custom label is given, override the default one (requestedDuration)
- badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
+ // build a label string. If a custom label is given, override the default one (requestedDuration)
+ badgeValues.label = filterAndJoin([
+ labelPrefix,
+ label ?? `Uptime (${requestedDuration}${labelSuffix})`,
+ ]);
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
}
@@ -290,7 +293,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
// build a lable string. If a custom label is given, override the default one (requestedDuration)
- badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
+ badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]);
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
}
@@ -304,4 +307,237 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
}
});
+router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async (request, response) => {
+ allowAllOrigin(response);
+
+ const {
+ label,
+ labelPrefix,
+ labelSuffix,
+ prefix,
+ suffix = badgeConstants.defaultPingValueSuffix,
+ color = badgeConstants.defaultPingColor,
+ labelColor,
+ style = badgeConstants.defaultStyle,
+ value, // for demo purpose only
+ } = request.query;
+
+ try {
+ const requestedMonitorId = parseInt(request.params.id, 10);
+
+ // Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
+ const requestedDuration = Math.min(
+ request.params.duration
+ ? parseInt(request.params.duration, 10)
+ : 24,
+ 720
+ );
+ const overrideValue = value && parseFloat(value);
+
+ const publicAvgPing = parseInt(await R.getCell(`
+ SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
+ WHERE monitor_group.group_id = \`group\`.id
+ AND heartbeat.time > DATETIME('now', ? || ' hours')
+ AND heartbeat.ping IS NOT NULL
+ AND public = 1
+ AND heartbeat.monitor_id = ?
+ `,
+ [ -requestedDuration, requestedMonitorId ]
+ ));
+
+ const badgeValues = { style };
+
+ if (!publicAvgPing) {
+ // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent
+
+ badgeValues.message = "N/A";
+ badgeValues.color = badgeConstants.naColor;
+ } else {
+ const avgPing = parseInt(overrideValue ?? publicAvgPing);
+
+ badgeValues.color = color;
+ // use a given, custom labelColor or use the default badge label color (defined by badge-maker)
+ badgeValues.labelColor = labelColor ?? "";
+ // build a label string. If a custom label is given, override the default one (requestedDuration)
+ badgeValues.label = filterAndJoin([
+ labelPrefix,
+ label ?? `Avg. Response (${requestedDuration}h)`,
+ labelSuffix,
+ ]);
+ badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
+ }
+
+ // build the SVG based on given values
+ const svg = makeBadge(badgeValues);
+
+ response.type("image/svg+xml");
+ response.send(svg);
+ } catch (error) {
+ send403(response, error.message);
+ }
+});
+
+router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, response) => {
+ allowAllOrigin(response);
+
+ const date = request.query.date;
+
+ const {
+ label,
+ labelPrefix,
+ labelSuffix,
+ prefix,
+ suffix = date ? "" : badgeConstants.defaultCertExpValueSuffix,
+ upColor = badgeConstants.defaultUpColor,
+ warnColor = badgeConstants.defaultWarnColor,
+ downColor = badgeConstants.defaultDownColor,
+ warnDays = badgeConstants.defaultCertExpireWarnDays,
+ downDays = badgeConstants.defaultCertExpireDownDays,
+ labelColor,
+ style = badgeConstants.defaultStyle,
+ value, // for demo purpose only
+ } = request.query;
+
+ try {
+ const requestedMonitorId = parseInt(request.params.id, 10);
+
+ const overrideValue = value && parseFloat(value);
+
+ let publicMonitor = await R.getRow(`
+ SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
+ WHERE monitor_group.group_id = \`group\`.id
+ AND monitor_group.monitor_id = ?
+ AND public = 1
+ `,
+ [ requestedMonitorId ]
+ );
+
+ const badgeValues = { style };
+
+ if (!publicMonitor) {
+ // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent
+
+ badgeValues.message = "N/A";
+ badgeValues.color = badgeConstants.naColor;
+ } else {
+ const tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
+ requestedMonitorId,
+ ]);
+
+ if (!tlsInfoBean) {
+ // return a "No/Bad Cert" badge in naColor (grey), if no cert saved (does not save bad certs?)
+ badgeValues.message = "No/Bad Cert";
+ badgeValues.color = badgeConstants.naColor;
+ } else {
+ const tlsInfo = JSON.parse(tlsInfoBean.info_json);
+
+ if (!tlsInfo.valid) {
+ // return a "Bad Cert" badge in naColor (grey), when cert is not valid
+ badgeValues.message = "Bad Cert";
+ badgeValues.color = badgeConstants.downColor;
+ } else {
+ const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
+
+ if (daysRemaining > warnDays) {
+ badgeValues.color = upColor;
+ } else if (daysRemaining > downDays) {
+ badgeValues.color = warnColor;
+ } else {
+ badgeValues.color = downColor;
+ }
+ // use a given, custom labelColor or use the default badge label color (defined by badge-maker)
+ badgeValues.labelColor = labelColor ?? "";
+ // build a label string. If a custom label is given, override the default one
+ badgeValues.label = filterAndJoin([
+ labelPrefix,
+ label ?? "Cert Exp.",
+ labelSuffix,
+ ]);
+ badgeValues.message = filterAndJoin([ prefix, date ? tlsInfo.certInfo.validTo : daysRemaining, suffix ]);
+ }
+ }
+ }
+
+ // build the SVG based on given values
+ const svg = makeBadge(badgeValues);
+
+ response.type("image/svg+xml");
+ response.send(svg);
+ } catch (error) {
+ send403(response, error.message);
+ }
+});
+
+router.get("/api/badge/:id/response", cache("5 minutes"), async (request, response) => {
+ allowAllOrigin(response);
+
+ const {
+ label,
+ labelPrefix,
+ labelSuffix,
+ prefix,
+ suffix = badgeConstants.defaultPingValueSuffix,
+ color = badgeConstants.defaultPingColor,
+ labelColor,
+ style = badgeConstants.defaultStyle,
+ value, // for demo purpose only
+ } = request.query;
+
+ try {
+ const requestedMonitorId = parseInt(request.params.id, 10);
+
+ const overrideValue = value && parseFloat(value);
+
+ let publicMonitor = await R.getRow(`
+ SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
+ WHERE monitor_group.group_id = \`group\`.id
+ AND monitor_group.monitor_id = ?
+ AND public = 1
+ `,
+ [ requestedMonitorId ]
+ );
+
+ const badgeValues = { style };
+
+ if (!publicMonitor) {
+ // return a "N/A" badge in naColor (grey), if monitor is not public / not available / non existent
+
+ badgeValues.message = "N/A";
+ badgeValues.color = badgeConstants.naColor;
+ } else {
+ const heartbeat = await Monitor.getPreviousHeartbeat(
+ requestedMonitorId
+ );
+
+ if (!heartbeat.ping) {
+ // return a "N/A" badge in naColor (grey), if previous heartbeat has no ping
+
+ badgeValues.message = "N/A";
+ badgeValues.color = badgeConstants.naColor;
+ } else {
+ const ping = parseInt(overrideValue ?? heartbeat.ping);
+
+ badgeValues.color = color;
+ // use a given, custom labelColor or use the default badge label color (defined by badge-maker)
+ badgeValues.labelColor = labelColor ?? "";
+ // build a label string. If a custom label is given, override the default one
+ badgeValues.label = filterAndJoin([
+ labelPrefix,
+ label ?? "Response",
+ labelSuffix,
+ ]);
+ badgeValues.message = filterAndJoin([ prefix, ping, suffix ]);
+ }
+ }
+
+ // build the SVG based on given values
+ const svg = makeBadge(badgeValues);
+
+ response.type("image/svg+xml");
+ response.send(svg);
+ } catch (error) {
+ send403(response, error.message);
+ }
+});
+
module.exports = router;
diff --git a/src/assets/app.scss b/src/assets/app.scss
index 7da76fff0..f550406fd 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -35,6 +35,11 @@ textarea.form-control {
color: $maintenance !important;
}
+.incident a,
+.bg-maintenance a {
+ color: inherit;
+}
+
.list-group {
border-radius: 0.75rem;
@@ -248,6 +253,11 @@ optgroup {
}
}
+ .incident a,
+ .bg-maintenance a {
+ color: inherit;
+ }
+
.form-control,
.form-control:focus,
.form-select,
diff --git a/src/lang/en.json b/src/lang/en.json
index df8288d22..8a195a2a9 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -25,6 +25,7 @@
"General Monitor Type": "General Monitor Type",
"Passive Monitor Type": "Passive Monitor Type",
"Specific Monitor Type": "Specific Monitor Type",
+ "markdownSupported": "Markdown syntax supported",
"pauseDashboardHome": "Pause",
"Pause": "Pause",
"Name": "Name",
diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue
index f0d87fe5a..00e649381 100644
--- a/src/pages/EditMaintenance.vue
+++ b/src/pages/EditMaintenance.vue
@@ -21,6 +21,9 @@
+
{{ $t("Powered by") }} {{ $t("Uptime Kuma" ) }} @@ -316,6 +327,8 @@ import ImageCropUpload from "vue-image-crop-upload"; import { PrismEditor } from "vue-prism-editor"; import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere import { useToast } from "vue-toastification"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; import Confirm from "../components/Confirm.vue"; import PublicGroupList from "../components/PublicGroupList.vue"; import MaintenanceTime from "../components/MaintenanceTime.vue"; @@ -483,6 +496,13 @@ export default { return this.overallStatus === STATUS_PAGE_MAINTENANCE; }, + incidentHTML() { + return DOMPurify.sanitize(marked(this.incident.content)); + }, + + footerHTML() { + return DOMPurify.sanitize(marked(this.config.footerText)); + }, }, watch: { @@ -842,6 +862,15 @@ export default { this.config.domainNameList.splice(index, 1); }, + /** + * Generate sanitized HTML from maintenance description + * @param {string} description + * @returns {string} Sanitized HTML + */ + maintenanceHTML(description) { + return DOMPurify.sanitize(marked(description)); + }, + } };