mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-27 22:49:42 -08:00
Merge remote-tracking branch 'origin/master' into feature-google-analytics
This commit is contained in:
commit
10c6f3b688
5
.github/workflows/auto-test.yml
vendored
5
.github/workflows/auto-test.yml
vendored
|
@ -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
|
||||
|
||||
|
|
7
.github/workflows/close-incorrect-issue.yml
vendored
7
.github/workflows/close-incorrect-issue.yml
vendored
|
@ -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'
|
||||
|
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
|
@ -9,7 +9,7 @@ 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.'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Uptime Kuma
|
||||
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/engage/uptime-kuma/">
|
||||
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
||||
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
<textarea
|
||||
id="description" v-model="maintenance.description" class="form-control"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affected Monitors -->
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<div class="my-3">
|
||||
<label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
|
||||
<textarea id="footer-text" v-model="config.footerText" class="form-control"></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check form-switch">
|
||||
|
@ -154,7 +157,12 @@
|
|||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||
|
||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
<Editable v-if="editIncidentMode" v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
<div v-if="editIncidentMode" class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-if="! editIncidentMode" class="content" v-html="incidentHTML"></div>
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
|
@ -242,7 +250,8 @@
|
|||
class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
|
||||
>
|
||||
<h4 class="alert-heading">{{ maintenance.title }}</h4>
|
||||
<div class="content">{{ maintenance.description }}</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div class="content" v-html="maintenanceHTML(maintenance.description)"></div>
|
||||
<MaintenanceTime :maintenance="maintenance" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -285,7 +294,9 @@
|
|||
<div class="custom-footer-text text-start">
|
||||
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
|
||||
</div>
|
||||
<Editable v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
|
||||
<Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-if="! enableEditMode" class="alert-heading p-2" v-html="footerHTML"></div>
|
||||
|
||||
<p v-if="config.showPoweredBy">
|
||||
{{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||
|
@ -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));
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue