mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-29 07:29:46 -08:00
Merge branch 'louislam:master' into italian-translation-update
This commit is contained in:
commit
0f4bc5850b
|
@ -20,6 +20,11 @@ yarn.lock
|
||||||
app.json
|
app.json
|
||||||
CODE_OF_CONDUCT.md
|
CODE_OF_CONDUCT.md
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
|
CNAME
|
||||||
|
install.sh
|
||||||
|
SECURITY.md
|
||||||
|
tsconfig.json
|
||||||
|
|
||||||
|
|
||||||
### .gitignore content (commented rules are duplicated)
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
|
|
13
README.md
13
README.md
|
@ -107,10 +107,21 @@ Telegram Notification Sample:
|
||||||
|
|
||||||
If you love this project, please consider giving me a ⭐.
|
If you love this project, please consider giving me a ⭐.
|
||||||
|
|
||||||
|
|
||||||
|
## 🗣️ Discussion
|
||||||
|
|
||||||
|
You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
|
I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments.
|
||||||
|
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
If you want to report a bug or request a new feature. Free feel to open a new issue.
|
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||||
|
|
||||||
|
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.
|
||||||
|
|
||||||
|
|
10
db/patch11.sql
Normal file
10
db/patch11.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- For sendHeartbeatList
|
||||||
|
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
|
||||||
|
|
||||||
|
-- For sendImportantHeartbeatList
|
||||||
|
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -24,7 +24,7 @@ RUN npm install --legacy-peer-deps && npm run build && npm prune
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=600s --timeout=130s --start-period=300s CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
|
|
|
@ -19,7 +19,7 @@ RUN npm install --legacy-peer-deps && npm run build && npm prune
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=600s --timeout=130s --start-period=300s CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
|
|
|
@ -11,7 +11,7 @@ if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
||||||
let options = {
|
let options = {
|
||||||
host: process.env.HOST || "127.0.0.1",
|
host: process.env.HOST || "127.0.0.1",
|
||||||
port: parseInt(process.env.PORT) || 3001,
|
port: parseInt(process.env.PORT) || 3001,
|
||||||
timeout: 120 * 1000,
|
timeout: 28 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = client.request(options, (res) => {
|
let request = client.request(options, (res) => {
|
||||||
|
|
|
@ -32,19 +32,16 @@ async function sendNotificationList(socket) {
|
||||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
let list = await R.find("heartbeat", `
|
let list = await R.getAll(`
|
||||||
monitor_id = ?
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`, [
|
`, [
|
||||||
monitorID,
|
monitorID,
|
||||||
])
|
])
|
||||||
|
|
||||||
let result = [];
|
let result = list.reverse();
|
||||||
|
|
||||||
for (let bean of list) {
|
|
||||||
result.unshift(bean.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toUser) {
|
if (toUser) {
|
||||||
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
|
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
|
||||||
|
|
|
@ -36,7 +36,11 @@ class Database {
|
||||||
|
|
||||||
// Change to WAL
|
// Change to WAL
|
||||||
await R.exec("PRAGMA journal_mode = WAL");
|
await R.exec("PRAGMA journal_mode = WAL");
|
||||||
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
|
|
||||||
|
console.log("SQLite config:");
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
|
console.log(await R.getAll("PRAGMA cache_size"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
|
|
|
@ -409,59 +409,60 @@ class Monitor extends BeanModel {
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async sendUptime(duration, io, monitorID, userID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
let sec = duration * 3600;
|
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
||||||
|
|
||||||
let heartbeatList = await R.getAll(`
|
// Handle if heartbeat duration longer than the target duration
|
||||||
SELECT duration, time, status
|
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
|
||||||
|
let result = await R.getRow(`
|
||||||
|
SELECT
|
||||||
|
-- SUM all duration, also trim off the beat out of time window
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||||
|
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||||
|
ELSE duration
|
||||||
|
END
|
||||||
|
) AS total_duration,
|
||||||
|
|
||||||
|
-- SUM all uptime duration, also trim off the beat out of time window
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN (status = 1)
|
||||||
|
THEN
|
||||||
|
CASE
|
||||||
|
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||||
|
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||||
|
ELSE duration
|
||||||
|
END
|
||||||
|
END
|
||||||
|
) AS uptime_duration
|
||||||
FROM heartbeat
|
FROM heartbeat
|
||||||
WHERE time > DATETIME('now', ? || ' hours')
|
WHERE time > ?
|
||||||
AND monitor_id = ? `, [
|
AND monitor_id = ?
|
||||||
-duration,
|
`, [
|
||||||
|
startTime, startTime, startTime, startTime, startTime,
|
||||||
monitorID,
|
monitorID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
||||||
|
|
||||||
let downtime = 0;
|
let totalDuration = result.total_duration;
|
||||||
let total = 0;
|
let uptimeDuration = result.uptime_duration;
|
||||||
let uptime;
|
let uptime = 0;
|
||||||
|
|
||||||
// Special handle for the first heartbeat only
|
|
||||||
if (heartbeatList.length === 1) {
|
|
||||||
|
|
||||||
if (heartbeatList[0].status === 1) {
|
|
||||||
uptime = 1;
|
|
||||||
} else {
|
|
||||||
uptime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
for (let row of heartbeatList) {
|
|
||||||
let value = parseInt(row.duration)
|
|
||||||
let time = row.time
|
|
||||||
|
|
||||||
// Handle if heartbeat duration longer than the target duration
|
|
||||||
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
|
|
||||||
if (value > sec) {
|
|
||||||
let trim = dayjs.utc().diff(dayjs(time), "second");
|
|
||||||
value = sec - trim;
|
|
||||||
|
|
||||||
if (value < 0) {
|
|
||||||
value = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
total += value;
|
|
||||||
if (row.status === 0 || row.status === 2) {
|
|
||||||
downtime += value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uptime = (total - downtime) / total;
|
|
||||||
|
|
||||||
|
if (totalDuration > 0) {
|
||||||
|
uptime = uptimeDuration / totalDuration;
|
||||||
if (uptime < 0) {
|
if (uptime < 0) {
|
||||||
uptime = 0;
|
uptime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||||
|
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||||
|
console.log("here???" + status);
|
||||||
|
if (status === UP) {
|
||||||
|
uptime = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
|
|
|
@ -30,10 +30,15 @@ class SMTP extends NotificationProvider {
|
||||||
|
|
||||||
// send mail with defined transport object
|
// send mail with defined transport object
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
from: notification.smtpFrom,
|
||||||
|
cc: notification.smtpCC,
|
||||||
|
bcc: notification.smtpBCC,
|
||||||
to: notification.smtpTo,
|
to: notification.smtpTo,
|
||||||
subject: msg,
|
subject: msg,
|
||||||
text: bodyTextContent,
|
text: bodyTextContent,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return "Sent Successfully.";
|
return "Sent Successfully.";
|
||||||
|
|
|
@ -593,6 +593,82 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("uploadBackup", async (uploadedJSON, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
let backupData = JSON.parse(uploadedJSON);
|
||||||
|
|
||||||
|
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`)
|
||||||
|
|
||||||
|
let notificationList = backupData.notificationList;
|
||||||
|
let monitorList = backupData.monitorList;
|
||||||
|
|
||||||
|
if (notificationList.length >= 1) {
|
||||||
|
for (let i = 0; i < notificationList.length; i++) {
|
||||||
|
let notification = JSON.parse(notificationList[i].config);
|
||||||
|
await Notification.save(notification, null, socket.userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitorList.length >= 1) {
|
||||||
|
for (let i = 0; i < monitorList.length; i++) {
|
||||||
|
let monitor = {
|
||||||
|
name: monitorList[i].name,
|
||||||
|
type: monitorList[i].type,
|
||||||
|
url: monitorList[i].url,
|
||||||
|
interval: monitorList[i].interval,
|
||||||
|
hostname: monitorList[i].hostname,
|
||||||
|
maxretries: monitorList[i].maxretries,
|
||||||
|
port: monitorList[i].port,
|
||||||
|
keyword: monitorList[i].keyword,
|
||||||
|
ignoreTls: monitorList[i].ignoreTls,
|
||||||
|
upsideDown: monitorList[i].upsideDown,
|
||||||
|
maxredirects: monitorList[i].maxredirects,
|
||||||
|
accepted_statuscodes: monitorList[i].accepted_statuscodes,
|
||||||
|
dns_resolve_type: monitorList[i].dns_resolve_type,
|
||||||
|
dns_resolve_server: monitorList[i].dns_resolve_server,
|
||||||
|
notificationIDList: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
let bean = R.dispense("monitor")
|
||||||
|
|
||||||
|
let notificationIDList = monitor.notificationIDList;
|
||||||
|
delete monitor.notificationIDList;
|
||||||
|
|
||||||
|
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
|
delete monitor.accepted_statuscodes;
|
||||||
|
|
||||||
|
bean.import(monitor)
|
||||||
|
bean.user_id = socket.userID
|
||||||
|
await R.store(bean)
|
||||||
|
|
||||||
|
await updateMonitorNotification(bean.id, notificationIDList)
|
||||||
|
|
||||||
|
if (monitorList[i].active == 1) {
|
||||||
|
await startMonitor(socket.userID, bean.id);
|
||||||
|
} else {
|
||||||
|
await pauseMonitor(socket.userID, bean.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendNotificationList(socket)
|
||||||
|
await sendMonitorList(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Backup successfully restored.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("clearEvents", async (monitorID, callback) => {
|
socket.on("clearEvents", async (monitorID, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Telegram v-if="notification.type === 'telegram'"></Telegram>
|
<Telegram v-if="notification.type === 'telegram'" />
|
||||||
|
|
||||||
<!-- TODO: Convert all into vue components, but not an easy task. -->
|
<!-- TODO: Convert all into vue components, but not an easy task. -->
|
||||||
|
|
||||||
|
@ -65,49 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'smtp'">
|
<SMTP v-if="notification.type === 'smtp'" />
|
||||||
<div class="mb-3">
|
|
||||||
<label for="hostname" class="form-label">Hostname</label>
|
|
||||||
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="port" class="form-label">Port</label>
|
|
||||||
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
|
|
||||||
<label class="form-check-label" for="secure">
|
|
||||||
Secure
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">
|
|
||||||
Generally, true for 465, false for other ports.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label">Username</label>
|
|
||||||
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<HiddenInput id="password" v-model="notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="from-email" class="form-label">From Email</label>
|
|
||||||
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="to-email" class="form-label">To Email</label>
|
|
||||||
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="notification.type === 'discord'">
|
<template v-if="notification.type === 'discord'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -437,8 +395,8 @@
|
||||||
|
|
||||||
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
|
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3 mt-4">
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider mb-4">
|
||||||
|
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input v-model="notification.isDefault" class="form-check-input" type="checkbox">
|
<input v-model="notification.isDefault" class="form-check-input" type="checkbox">
|
||||||
|
@ -456,6 +414,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
{{ $t("Delete") }}
|
{{ $t("Delete") }}
|
||||||
|
@ -481,19 +440,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Modal } from "bootstrap"
|
import { Modal } from "bootstrap"
|
||||||
import { ucfirst } from "../util.ts"
|
import { ucfirst } from "../util.ts"
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
import HiddenInput from "./HiddenInput.vue";
|
import HiddenInput from "./HiddenInput.vue";
|
||||||
import Telegram from "./notifications/Telegram.vue";
|
import Telegram from "./notifications/Telegram.vue";
|
||||||
import { useToast } from "vue-toastification"
|
import SMTP from "./notifications/SMTP.vue";
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Confirm,
|
Confirm,
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
Telegram,
|
Telegram,
|
||||||
|
SMTP,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
|
@ -504,8 +462,8 @@ export default {
|
||||||
notification: {
|
notification: {
|
||||||
name: "",
|
name: "",
|
||||||
type: null,
|
type: null,
|
||||||
gotifyPriority: 8,
|
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
|
// Do not set default value here, please scroll to show()
|
||||||
},
|
},
|
||||||
appriseInstalled: false,
|
appriseInstalled: false,
|
||||||
}
|
}
|
||||||
|
@ -558,9 +516,10 @@ export default {
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default set to Telegram
|
// Set Default value here
|
||||||
this.notification.type = "telegram"
|
this.notification.type = "telegram";
|
||||||
this.notification.gotifyPriority = 8
|
this.notification.gotifyPriority = 8;
|
||||||
|
this.notification.smtpSecure = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modal.show()
|
this.modal.show()
|
||||||
|
|
75
src/components/notifications/SMTP.vue
Normal file
75
src/components/notifications/SMTP.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||||
|
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||||
|
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="secure" class="form-label">Secure</label>
|
||||||
|
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
|
||||||
|
<option :value="false">None / STARTTLS (25, 587)</option>
|
||||||
|
<option :value="true">TLS (465)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
|
||||||
|
<label class="form-check-label" for="ignore-tls-error">
|
||||||
|
Ignore TLS Error
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">{{ $t("Username") }}</label>
|
||||||
|
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">{{ $t("Password") }}</label>
|
||||||
|
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="from-email" class="form-label">From Email</label>
|
||||||
|
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder=""Uptime Kuma" <example@kuma.pet>">
|
||||||
|
<div class="form-text">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-email" class="form-label">To Email</label>
|
||||||
|
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-cc" class="form-label">CC</label>
|
||||||
|
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="to-bcc" class="form-label">BCC</label>
|
||||||
|
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: "smtp",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -113,11 +113,19 @@ export default {
|
||||||
"Create your admin account": "Erstelle dein Admin Konto",
|
"Create your admin account": "Erstelle dein Admin Konto",
|
||||||
"Repeat Password": "Wiederhole das Passwort",
|
"Repeat Password": "Wiederhole das Passwort",
|
||||||
"Resource Record Type": "Resource Record Type",
|
"Resource Record Type": "Resource Record Type",
|
||||||
|
"Import/Export Backup": "Import/Export Backup",
|
||||||
|
"Export": "Export",
|
||||||
|
"Import": "Import",
|
||||||
respTime: "Antw. Zeit (ms)",
|
respTime: "Antw. Zeit (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
"Default enabled": "Standardmäßig aktiviert",
|
"Default enabled": "Standardmäßig aktiviert",
|
||||||
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
|
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
|
||||||
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
|
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
|
||||||
Create: "Erstellen",
|
Create: "Erstellen",
|
||||||
"Auto Get": "Auto Get"
|
"Auto Get": "Auto Get",
|
||||||
|
backupDescription: "Es können alle Monitore und alle Benachrichtigungen in einer JSON-Datei gesichert werden.",
|
||||||
|
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
|
||||||
|
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
||||||
|
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
||||||
|
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,9 @@ export default {
|
||||||
"Last Result": "Last Result",
|
"Last Result": "Last Result",
|
||||||
"Create your admin account": "Create your admin account",
|
"Create your admin account": "Create your admin account",
|
||||||
"Repeat Password": "Repeat Password",
|
"Repeat Password": "Repeat Password",
|
||||||
|
"Import/Export Backup": "Import/Export Backup",
|
||||||
|
"Export": "Export",
|
||||||
|
"Import": "Import",
|
||||||
respTime: "Resp. Time (ms)",
|
respTime: "Resp. Time (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Default enabled",
|
||||||
|
@ -119,5 +122,10 @@ export default {
|
||||||
"Clear Data": "Clear Data",
|
"Clear Data": "Clear Data",
|
||||||
Events: "Events",
|
Events: "Events",
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Heartbeats",
|
||||||
"Auto Get": "Auto Get"
|
"Auto Get": "Auto Get",
|
||||||
|
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
||||||
|
backupDescription2: "PS: History and event data is not included.",
|
||||||
|
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
||||||
|
alertNoFile: "Please select a file to import.",
|
||||||
|
alertWrongFileType: "Please select a JSON file.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,10 @@ export default {
|
||||||
this.importantHeartbeatList = {}
|
this.importantHeartbeatList = {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadBackup(uploadedJSON, callback) {
|
||||||
|
socket.emit("uploadBackup", uploadedJSON, callback)
|
||||||
|
},
|
||||||
|
|
||||||
clearEvents(monitorID, callback) {
|
clearEvents(monitorID, callback) {
|
||||||
socket.emit("clearEvents", monitorID, callback)
|
socket.emit("clearEvents", monitorID, callback)
|
||||||
},
|
},
|
||||||
|
|
|
@ -120,6 +120,27 @@
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ $t("backupDescription") }} <br />
|
||||||
|
({{ $t("backupDescription2") }}) <br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<button class="btn btn-outline-primary" @click="downloadBackup">{{ $t("Export") }}</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" :disabled="processing" @click="importBackup">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Import") }}
|
||||||
|
</button>
|
||||||
|
<input id="importBackup" type="file" class="form-control" accept="application/json">
|
||||||
|
</div>
|
||||||
|
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
|
||||||
|
{{ importAlert }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>{{ $t("backupDescription3") }}</strong></p>
|
||||||
|
|
||||||
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
@ -275,6 +296,8 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
importAlert: null,
|
||||||
|
processing: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -351,6 +374,52 @@ export default {
|
||||||
this.$root.storage().removeItem("token");
|
this.$root.storage().removeItem("token");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
downloadBackup() {
|
||||||
|
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||||
|
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||||
|
let monitorList = Object.values(this.$root.monitorList);
|
||||||
|
let exportData = {
|
||||||
|
version: this.$root.info.version,
|
||||||
|
notificationList: this.$root.notificationList,
|
||||||
|
monitorList: monitorList,
|
||||||
|
}
|
||||||
|
exportData = JSON.stringify(exportData);
|
||||||
|
let downloadItem = document.createElement("a");
|
||||||
|
downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData));
|
||||||
|
downloadItem.setAttribute("download", fileName);
|
||||||
|
downloadItem.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
importBackup() {
|
||||||
|
this.processing = true;
|
||||||
|
let uploadItem = document.getElementById("importBackup").files;
|
||||||
|
|
||||||
|
if (uploadItem.length <= 0) {
|
||||||
|
this.processing = false;
|
||||||
|
return this.importAlert = this.$t("alertNoFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadItem.item(0).type !== "application/json") {
|
||||||
|
this.processing = false;
|
||||||
|
return this.importAlert = this.$t("alertWrongFileType")
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileReader = new FileReader();
|
||||||
|
fileReader.readAsText(uploadItem.item(0));
|
||||||
|
|
||||||
|
fileReader.onload = item => {
|
||||||
|
this.$root.uploadBackup(item.target.result, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(res.msg);
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
clearStatistics() {
|
clearStatistics() {
|
||||||
this.$root.clearStatistics((res) => {
|
this.$root.clearStatistics((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -388,6 +457,18 @@ export default {
|
||||||
.btn-check:hover + .btn-outline-primary {
|
.btn-check:hover + .btn-outline-primary {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#importBackup {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: $primary;
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
|
Loading…
Reference in a new issue