mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-28 01:04:05 +00:00
Merge branch 'master' into andreasbrett_patch-1
# Conflicts: # src/util.js
This commit is contained in:
commit
f2f8f33b86
50 changed files with 1758 additions and 317 deletions
12
README.md
12
README.md
|
@ -16,7 +16,7 @@ Try it!
|
|||
|
||||
https://demo.uptime.kuma.pet
|
||||
|
||||
It is a 10 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
|
||||
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
|
||||
|
||||
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
|
||||
|
||||
|
@ -25,7 +25,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
|
|||
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
|
||||
* 20 seconds interval.
|
||||
* 20 second intervals.
|
||||
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||
* Simple Status Page
|
||||
* Ping Chart
|
||||
|
@ -40,7 +40,7 @@ docker volume create uptime-kuma
|
|||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### 💪🏻 Without Docker
|
||||
|
||||
|
@ -58,11 +58,11 @@ npm run setup
|
|||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have: npm install pm2 -g
|
||||
# Install PM2 if you don't have it: npm install pm2 -g
|
||||
pm2 start server/server.js --name uptime-kuma
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
Browse to http://localhost:3001 after starting.
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
|
@ -124,7 +124,7 @@ You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-k
|
|||
### Subreddit
|
||||
|
||||
My Reddit account: louislamlam
|
||||
You can mention me if you ask question on Reddit.
|
||||
You can mention me if you ask a question on Reddit.
|
||||
https://www.reddit.com/r/UptimeKuma/
|
||||
|
||||
## Contribute
|
||||
|
|
|
@ -9,8 +9,8 @@ currently being supported with security updates.
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.7.X | :white_check_mark: |
|
||||
| < 1.7 | ❌ |
|
||||
| 1.8.X | :white_check_mark: |
|
||||
| <= 1.7.X | ❌ |
|
||||
|
||||
### Upgradable Docker Tags
|
||||
|
||||
|
|
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
13
db/patch-http-monitor-method-body-and-headers.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD method TEXT default 'GET' not null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD body TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD headers TEXT default null;
|
||||
|
||||
COMMIT;
|
|
@ -12,50 +12,59 @@ const rl = readline.createInterface({
|
|||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const main = async () => {
|
||||
Database.init(args);
|
||||
await Database.connect();
|
||||
|
||||
try {
|
||||
const user = await R.findOne("user");
|
||||
|
||||
if (! user) {
|
||||
throw new Error("user not found, have you installed?");
|
||||
}
|
||||
|
||||
console.log("Found user: " + user.username);
|
||||
|
||||
while (true) {
|
||||
let password = await question("New Password: ");
|
||||
let confirmPassword = await question("Confirm New Password: ");
|
||||
|
||||
if (password === confirmPassword) {
|
||||
await user.resetPassword(password);
|
||||
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
rl.close();
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
const user = await R.findOne("user");
|
||||
if (! user) {
|
||||
throw new Error("user not found, have you installed?");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Password reset successfully.");
|
||||
console.log("Found user: " + user.username);
|
||||
|
||||
while (true) {
|
||||
let password = await question("New Password: ");
|
||||
let confirmPassword = await question("Confirm New Password: ");
|
||||
|
||||
if (password === confirmPassword) {
|
||||
await user.resetPassword(password);
|
||||
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
}
|
||||
}
|
||||
console.log("Password reset successfully.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error: " + e.message);
|
||||
}
|
||||
|
||||
await Database.close();
|
||||
rl.close();
|
||||
|
||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
||||
})();
|
||||
console.log("Finished.");
|
||||
};
|
||||
|
||||
function question(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
};
|
||||
|
|
|
@ -26,10 +26,12 @@ const copyRecursiveSync = function (src, dest) {
|
|||
}
|
||||
};
|
||||
|
||||
console.log("Arguments:", process.argv)
|
||||
console.log("Arguments:", process.argv);
|
||||
const baseLangCode = process.argv[2] || "en";
|
||||
console.log("Base Lang: " + baseLangCode);
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
if (fs.existsSync("./languages")) {
|
||||
fs.rmdirSync("./languages", { recursive: true });
|
||||
}
|
||||
copyRecursiveSync("../../src/languages", "./languages");
|
||||
|
||||
const en = (await import("./languages/en.js")).default;
|
||||
|
@ -39,7 +41,7 @@ console.log("Files:", files);
|
|||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".js")) {
|
||||
console.log("Skipping " + file)
|
||||
console.log("Skipping " + file);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
611
package-lock.json
generated
611
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -60,9 +60,10 @@
|
|||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.21.4",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "~5.1.1",
|
||||
"chardet": "^1.3.0",
|
||||
"bree": "~6.3.1",
|
||||
"chart.js": "~3.5.1",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"command-exists": "~1.2.9",
|
||||
|
@ -72,6 +73,7 @@
|
|||
"express-basic-auth": "~1.2.0",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"nodemailer": "~6.6.5",
|
||||
"notp": "~2.0.3",
|
||||
|
@ -108,6 +110,7 @@
|
|||
"@vitejs/plugin-legacy": "~1.6.1",
|
||||
"@vitejs/plugin-vue": "~1.9.2",
|
||||
"@vue/compiler-sfc": "~3.2.19",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.1",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
|
|
7
server/config.js
Normal file
7
server/config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const args = require("args-parser")(process.argv);
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
demoMode
|
||||
};
|
|
@ -49,10 +49,11 @@ class Database {
|
|||
"patch-incident-table.sql": true,
|
||||
"patch-group-table.sql": true,
|
||||
"patch-monitor-push_token.sql": true,
|
||||
"patch-http-monitor-method-body-and-headers.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
* The finally version should be 10 after merged tag feature
|
||||
* The final version should be 10 after merged tag feature
|
||||
* @deprecated Use patchList for any new feature
|
||||
*/
|
||||
static latestVersion = 10;
|
||||
|
|
31
server/jobs.js
Normal file
31
server/jobs.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
|
||||
const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
interval: "at 03:14",
|
||||
}
|
||||
];
|
||||
|
||||
const initBackgroundJobs = function (args) {
|
||||
const bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
jobs,
|
||||
worker: {
|
||||
env: SHARE_ENV,
|
||||
workerData: args,
|
||||
},
|
||||
workerMessageHandler: (message) => {
|
||||
console.log("[Background Job]:", message);
|
||||
}
|
||||
});
|
||||
|
||||
bree.start();
|
||||
return bree;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initBackgroundJobs
|
||||
};
|
40
server/jobs/clear-old-data.js
Normal file
40
server/jobs/clear-old-data.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const { log, exit, connectDb } = require("./util-worker");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
|
||||
(async () => {
|
||||
await connectDb();
|
||||
|
||||
let period = await setting("keepDataPeriodDays");
|
||||
|
||||
// Set Default Period
|
||||
if (period == null) {
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
period = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
// Try parse setting
|
||||
let parsedPeriod;
|
||||
try {
|
||||
parsedPeriod = parseInt(period);
|
||||
} catch (_) {
|
||||
log("Failed to parse setting, resetting to default..");
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
try {
|
||||
await R.exec(
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[parsedPeriod]
|
||||
);
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
|
||||
exit();
|
||||
})();
|
39
server/jobs/util-worker.js
Normal file
39
server/jobs/util-worker.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
const exit = function (error) {
|
||||
if (error && error != 0) {
|
||||
process.exit(error);
|
||||
} else {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage("done");
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
);
|
||||
|
||||
Database.init({
|
||||
"data-dir": dbPath,
|
||||
});
|
||||
|
||||
await Database.connect();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
log,
|
||||
exit,
|
||||
connectDb,
|
||||
};
|
|
@ -7,11 +7,11 @@ dayjs.extend(timezone);
|
|||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
const { demoMode } = require("../server");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
|
@ -55,6 +55,9 @@ class Monitor extends BeanModel {
|
|||
id: this.id,
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
|
@ -138,11 +141,15 @@ class Monitor extends BeanModel {
|
|||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
let res = await axios.get(this.url, {
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
|
@ -152,7 +159,8 @@ class Monitor extends BeanModel {
|
|||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
});
|
||||
};
|
||||
let res = await axios.request(options);
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
|
@ -172,6 +180,10 @@ class Monitor extends BeanModel {
|
|||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
|
||||
console.log(res.data);
|
||||
}
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
} else {
|
||||
|
@ -260,6 +272,46 @@ class Monitor extends BeanModel {
|
|||
return;
|
||||
}
|
||||
|
||||
} else if (this.type === "steam") {
|
||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||
const steamAPIKey = await setting("steamAPIKey");
|
||||
const filter = `addr\\${this.hostname}:${this.port}`;
|
||||
|
||||
if (!steamAPIKey) {
|
||||
throw new Error("Steam API Key not found");
|
||||
}
|
||||
|
||||
let res = await axios.get(steamApiUrl, {
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
}),
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
},
|
||||
params: {
|
||||
filter: filter,
|
||||
key: steamAPIKey,
|
||||
}
|
||||
});
|
||||
|
||||
if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.response.servers[0].name;
|
||||
|
||||
try {
|
||||
bean.ping = await ping(this.hostname);
|
||||
} catch (_) { }
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
|
@ -292,54 +344,13 @@ class Monitor extends BeanModel {
|
|||
|
||||
let beatInterval = this.interval;
|
||||
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
@ -546,6 +557,53 @@ class Monitor extends BeanModel {
|
|||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
|
||||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
|
||||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
]);
|
||||
|
||||
let text;
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up";
|
||||
} else {
|
||||
text = "🔴 Down";
|
||||
}
|
||||
|
||||
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Status Page Cache
|
||||
apicache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
|
108
server/notification-providers/aliyun-sms.js
Normal file
108
server/notification-providers/aliyun-sms.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
const qs = require("qs");
|
||||
|
||||
class AliyunSMS extends NotificationProvider {
|
||||
name = "AliyunSMS";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let msgBody = JSON.stringify({
|
||||
name: monitorJSON["name"],
|
||||
time: heartbeatJSON["time"],
|
||||
status: this.statusToString(heartbeatJSON["status"]),
|
||||
msg: heartbeatJSON["msg"],
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let msgBody = JSON.stringify({
|
||||
name: "",
|
||||
time: "",
|
||||
status: "",
|
||||
msg: msg,
|
||||
});
|
||||
if (this.sendSms(notification, msgBody)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
PhoneNumbers: notification.phonenumber,
|
||||
TemplateCode: notification.templateCode,
|
||||
SignName: notification.signName,
|
||||
TemplateParam: msgbody,
|
||||
AccessKeyId: notification.accessKeyId,
|
||||
Format: "JSON",
|
||||
SignatureMethod: "HMAC-SHA1",
|
||||
SignatureVersion: "1.0",
|
||||
SignatureNonce: Math.random().toString(),
|
||||
Timestamp: new Date().toISOString(),
|
||||
Action: "SendSms",
|
||||
Version: "2017-05-25",
|
||||
};
|
||||
|
||||
params.Signature = this.sign(params, notification.secretAccessKey);
|
||||
let config = {
|
||||
method: "POST",
|
||||
url: "http://dysmsapi.aliyuncs.com/",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: qs.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.Message == "OK") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Aliyun request sign */
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
let data = [];
|
||||
|
||||
let oa = Object.keys(param).sort();
|
||||
|
||||
for (let i = 0; i < oa.length; i++) {
|
||||
let key = oa[i];
|
||||
param2[key] = param[key];
|
||||
}
|
||||
|
||||
for (let key in param2) {
|
||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||
}
|
||||
|
||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||
return Crypto
|
||||
.createHmac("sha1", `${AccessKeySecret}&`)
|
||||
.update(Buffer.from(StringToSign))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AliyunSMS;
|
79
server/notification-providers/dingding.js
Normal file
79
server/notification-providers/dingding.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { default: axios } = require("axios");
|
||||
const Crypto = require("crypto");
|
||||
|
||||
class DingDing extends NotificationProvider {
|
||||
name = "DingDing";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
let params = {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: monitorJSON["name"],
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
let params = {
|
||||
msgtype: "text",
|
||||
text: {
|
||||
content: msg
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
|
||||
let config = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
|
||||
data: JSON.stringify(params),
|
||||
};
|
||||
|
||||
let result = await axios(config);
|
||||
if (result.data.errmsg == "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** DingDing sign */
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
case UP:
|
||||
return "UP";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DingDing;
|
|
@ -39,8 +39,9 @@ class Slack extends NotificationProvider {
|
|||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
|
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
|
|||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
// Lets start with default subject and empty string for custom one
|
||||
let subject = msg;
|
||||
|
||||
// Change the subject if:
|
||||
// - The msg ends with "Testing" or
|
||||
// - Actual Up/Down Notification
|
||||
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||
let customSubject = "";
|
||||
|
||||
// Our subject cannot end with whitespace it's often raise spam score
|
||||
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
|
||||
if (notification.customSubject) {
|
||||
customSubject = notification.customSubject.trim();
|
||||
}
|
||||
|
||||
// If custom subject is not empty, change subject for notification
|
||||
if (customSubject !== "") {
|
||||
|
||||
// Replace "MACROS" with corresponding variable
|
||||
let replaceName = new RegExp("{{NAME}}", "g");
|
||||
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
|
||||
let replaceStatus = new RegExp("{{STATUS}}", "g");
|
||||
|
||||
// Lets start with dummy values to simplify code
|
||||
let monitorName = "Test";
|
||||
let monitorHostnameOrURL = "testing.hostname";
|
||||
let serviceStatus = "⚠️ Test";
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeatJSON !== null) {
|
||||
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||
}
|
||||
|
||||
// Break replace to one by line for better readability
|
||||
customSubject = customSubject.replace(replaceStatus, serviceStatus);
|
||||
customSubject = customSubject.replace(replaceName, monitorName);
|
||||
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
|
||||
|
||||
subject = customSubject;
|
||||
}
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
|
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
|
|||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
|
|
|
@ -19,6 +19,8 @@ const Teams = require("./notification-providers/teams");
|
|||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
|
||||
class Notification {
|
||||
|
||||
|
@ -31,6 +33,8 @@ class Notification {
|
|||
|
||||
const list = [
|
||||
new Apprise(),
|
||||
new AliyunSms(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Teams(),
|
||||
new Gotify(),
|
||||
|
|
|
@ -4,10 +4,7 @@ const net = require("net");
|
|||
const spawn = require("child_process").spawn;
|
||||
const events = require("events");
|
||||
const fs = require("fs");
|
||||
const WIN = /^win/.test(process.platform);
|
||||
const LIN = /^linux/.test(process.platform);
|
||||
const MAC = /^darwin/.test(process.platform);
|
||||
const FBSD = /^freebsd/.test(process.platform);
|
||||
const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
|
@ -23,12 +20,12 @@ function Ping(host, options) {
|
|||
|
||||
const timeout = 10;
|
||||
|
||||
if (WIN) {
|
||||
if (util.WIN) {
|
||||
this._bin = "c:/windows/system32/ping.exe";
|
||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||
|
||||
} else if (LIN) {
|
||||
} else if (util.LIN) {
|
||||
this._bin = "/bin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||
|
@ -40,7 +37,7 @@ function Ping(host, options) {
|
|||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (MAC) {
|
||||
} else if (util.MAC) {
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
this._bin = "/sbin/ping6";
|
||||
|
@ -51,7 +48,7 @@ function Ping(host, options) {
|
|||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
} else if (util.FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
||||
|
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
|
|||
});
|
||||
|
||||
this._ping.stdout.on("data", function (data) { // log stdout
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stdout = (this._stdout || "") + data;
|
||||
});
|
||||
|
||||
|
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
|||
});
|
||||
|
||||
this._ping.stderr.on("data", function (data) { // log stderr
|
||||
if (util.WIN) {
|
||||
data = convertOutput(data);
|
||||
}
|
||||
this._stderr = (this._stderr || "") + data;
|
||||
});
|
||||
|
||||
|
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
|||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
||||
* Thank @pemassi
|
||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
||||
* @param data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
if (util.WIN) {
|
||||
if (data) {
|
||||
return util.convertToUTF8(data);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const server = require("../server");
|
|||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP } = require("../../src/util");
|
||||
const { UP, flipStatus, debug } = require("../../src/util");
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
|
@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
|
|||
|
||||
router.get("/api/push/:pushToken", async (request, response) => {
|
||||
try {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping;
|
||||
let ping = request.query.ping || null;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
|
@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||
throw new Error("Monitor not found or not active.");
|
||||
}
|
||||
|
||||
const previousHeartbeat = await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitor.id
|
||||
]);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
status = flipStatus(status);
|
||||
}
|
||||
|
||||
let isFirstBeat = true;
|
||||
let previousStatus = status;
|
||||
let duration = 0;
|
||||
|
||||
let bean = R.dispense("heartbeat");
|
||||
bean.monitor_id = monitor.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = UP;
|
||||
|
||||
if (previousHeartbeat) {
|
||||
isFirstBeat = false;
|
||||
previousStatus = previousHeartbeat.status;
|
||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||
}
|
||||
|
||||
debug("PreviousStatus: " + previousStatus);
|
||||
debug("Current Status: " + status);
|
||||
|
||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
||||
bean.monitor_id = monitor.id;
|
||||
bean.status = status;
|
||||
bean.msg = msg;
|
||||
bean.ping = ping;
|
||||
bean.duration = duration;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
|
@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
|||
response.json({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
if (bean.important) {
|
||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
response.json({
|
||||
ok: false,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
console.log("Welcome to Uptime Kuma");
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
debug(args);
|
||||
|
||||
|
@ -8,10 +9,6 @@ if (! process.env.NODE_ENV) {
|
|||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
|
||||
// Demo Mode?
|
||||
const demoMode = args["demo"] || false;
|
||||
exports.demoMode = demoMode;
|
||||
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
console.log("Importing Node libraries");
|
||||
|
@ -43,7 +40,7 @@ console.log("Importing this project modules");
|
|||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
debug("Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
|
||||
|
||||
debug("Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
|
@ -52,6 +49,9 @@ Notification.init();
|
|||
debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
@ -61,12 +61,28 @@ console.info("Version: " + checkVersion.version);
|
|||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
if (!hostname && !FBSD) {
|
||||
hostname = process.env.HOST;
|
||||
}
|
||||
|
||||
if (hostname) {
|
||||
console.log("Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
|
||||
|
||||
// SSL
|
||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
|
||||
// 2FA / notp verification defaults
|
||||
const twofa_verification_opts = {
|
||||
"window": 1,
|
||||
"time": 30
|
||||
};
|
||||
|
||||
/**
|
||||
* Run unit test after the server is ready
|
||||
|
@ -74,7 +90,7 @@ const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
|||
*/
|
||||
const testMode = !!args["test"] || false;
|
||||
|
||||
if (demoMode) {
|
||||
if (config.demoMode) {
|
||||
console.log("==== Demo Mode ====");
|
||||
}
|
||||
|
||||
|
@ -265,7 +281,7 @@ exports.entryPage = "dashboard";
|
|||
}
|
||||
|
||||
if (data.token) {
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret);
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (verify && verify.delta == 0) {
|
||||
callback({
|
||||
|
@ -383,7 +399,7 @@ exports.entryPage = "dashboard";
|
|||
socket.userID,
|
||||
]);
|
||||
|
||||
let verify = notp.totp.verify(token, user.twofa_secret);
|
||||
let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
|
||||
|
||||
if (verify && verify.delta == 0) {
|
||||
callback({
|
||||
|
@ -509,6 +525,9 @@ exports.entryPage = "dashboard";
|
|||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
|
@ -1034,6 +1053,9 @@ exports.entryPage = "dashboard";
|
|||
name: monitorListData[i].name,
|
||||
type: monitorListData[i].type,
|
||||
url: monitorListData[i].url,
|
||||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
hostname: monitorListData[i].hostname,
|
||||
|
@ -1239,6 +1261,8 @@ exports.entryPage = "dashboard";
|
|||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
|
||||
})();
|
||||
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
|
|
|
@ -6,6 +6,14 @@ const passwordHash = require("./password-hash");
|
|||
const dayjs = require("dayjs");
|
||||
const { Resolver } = require("dns");
|
||||
const child_process = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
exports.LIN = /^linux/.test(process.platform);
|
||||
exports.MAC = /^darwin/.test(process.platform);
|
||||
exports.FBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
|
@ -116,7 +124,7 @@ exports.setting = async function (key) {
|
|||
}
|
||||
};
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
exports.setSetting = async function (key, value, type = null) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
]);
|
||||
|
@ -124,6 +132,7 @@ exports.setSetting = async function (key, value) {
|
|||
bean = R.dispense("setting");
|
||||
bean.key = key;
|
||||
}
|
||||
bean.type = type;
|
||||
bean.value = JSON.stringify(value);
|
||||
await R.store(bean);
|
||||
};
|
||||
|
@ -312,3 +321,14 @@ exports.startUnitTest = async () => {
|
|||
process.exit(code);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param body : Buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.convertToUTF8 = (body) => {
|
||||
const guessEncoding = chardet.detect(body);
|
||||
//debug("Guess Encoding: " + guessEncoding);
|
||||
const str = iconv.decode(body, guessEncoding);
|
||||
return str.toString();
|
||||
};
|
||||
|
|
|
@ -14,6 +14,10 @@ h2 {
|
|||
font-size: 26px;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
border-radius: 19px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
.multiselect__tag {
|
||||
border-radius: 50rem;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: 0;
|
||||
padding: 6px 26px 6px 10px;
|
||||
background: $primary !important;
|
||||
|
|
|
@ -186,7 +186,7 @@ export default {
|
|||
.beat {
|
||||
display: inline-block;
|
||||
background-color: $primary;
|
||||
border-radius: 50rem;
|
||||
border-radius: $border-radius;
|
||||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
|
|
25
src/components/notifications/AliyunSms.vue
Normal file
25
src/components/notifications/AliyunSms.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
|
||||
|
||||
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
|
||||
|
||||
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
|
||||
|
||||
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
16
src/components/notifications/DingDing.vue
Normal file
16
src/components/notifications/DingDing.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||
|
||||
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text">
|
||||
<p>For safety, must use secret key</p>
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -5,7 +5,7 @@
|
|||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||
<a
|
||||
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||
target="_blank"
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="homeserver-url" class="form-label">Homeserver URL (with http(s):// and optionally port)</label><span style="color: red;"><sup>*</sup></span>
|
||||
<label for="homeserver-url" class="form-label">{{ $t("matrixHomeserverURL") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="internal-room-id" class="form-label">Internal Room Id</label><span style="color: red;"><sup>*</sup></span>
|
||||
<label for="internal-room-id" class="form-label">{{ $t("Internal Room Id") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="access-token" class="form-label">Access Token</label><span style="color: red;"><sup>*</sup></span>
|
||||
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
|
||||
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<p style="margin-top: 8px;">
|
||||
You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running <code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
|
||||
{{ $t("matrixDesc1") }}
|
||||
</p>
|
||||
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
|
||||
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -30,5 +30,5 @@ export default {
|
|||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Do you use the legacy version of Octopush (2011-2020) or the new version?
|
||||
{{ $t("octopushLegacyHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
||||
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
||||
</select>
|
||||
<i18n-t tag="div" keypath="Check PromoSMS prices" class="form-text">
|
||||
<div class="form-text">
|
||||
{{ $t("checkPrice", [$t("promosms")]) }}
|
||||
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
||||
|
@ -25,7 +26,6 @@
|
|||
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
|
||||
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -57,6 +57,18 @@
|
|||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
||||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div v-pre class="form-text">
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{URL}}: URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<div class="mb-3">
|
||||
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
||||
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<div class="form-text">
|
||||
{{ $t("You can get a token from") }} <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
||||
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<p>"application/json" is good for any modern http servers such as express.js</p>
|
||||
<p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
|
||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||
<template #multipart>"multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
|
|
|
@ -18,6 +18,8 @@ import Pushbullet from "./Pushbullet.vue";
|
|||
import Line from "./Line.vue";
|
||||
import Mattermost from "./Mattermost.vue";
|
||||
import Matrix from "./Matrix.vue";
|
||||
import AliyunSMS from "./AliyunSms.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
|
@ -40,11 +42,13 @@ const NotificationFormList = {
|
|||
"promosms": PromoSMS,
|
||||
"lunasea": LunaSea,
|
||||
"Feishu": Feishu,
|
||||
"AliyunSMS": AliyunSMS,
|
||||
"apprise": Apprise,
|
||||
"pushbullet": Pushbullet,
|
||||
"line": Line,
|
||||
"mattermost": Mattermost,
|
||||
"matrix": Matrix,
|
||||
"DingDing": DingDing
|
||||
}
|
||||
|
||||
export default NotificationFormList
|
||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
|||
Appearance: "Appearance",
|
||||
Theme: "Theme",
|
||||
General: "General",
|
||||
"Primary Base URL": "Primary Base URL",
|
||||
Version: "Version",
|
||||
"Check Update On GitHub": "Check Update On GitHub",
|
||||
List: "List",
|
||||
|
@ -75,6 +76,9 @@ export default {
|
|||
"Upside Down Mode": "Upside Down Mode",
|
||||
"Max. Redirects": "Max. Redirects",
|
||||
"Accepted Status Codes": "Accepted Status Codes",
|
||||
"Push URL": "Push URL",
|
||||
needPushEvery: "You should call this url every {0} seconds.",
|
||||
pushOptionalParams: "Optional parameters: {0}",
|
||||
Save: "Save",
|
||||
Notifications: "Notifications",
|
||||
"Not available, please setup.": "Not available, please setup.",
|
||||
|
@ -185,7 +189,7 @@ export default {
|
|||
"Required": "Required",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
"You can get a token from": "You can get a token from",
|
||||
wayToGetTelegramToken: "You can get a token from {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
|
||||
wayToGetTelegramChatID: "You can get your chat id by sending message to the bot and go to this url to view the chat_id:",
|
||||
|
@ -201,6 +205,7 @@ export default {
|
|||
secureOptionTLS: "TLS (465)",
|
||||
"Ignore TLS Error": "Ignore TLS Error",
|
||||
"From Email": "From Email",
|
||||
emailCustomSubject: "Custom Subject",
|
||||
"To Email": "To Email",
|
||||
smtpCC: "CC",
|
||||
smtpBCC: "BCC",
|
||||
|
@ -251,6 +256,8 @@ export default {
|
|||
"SMS Type": "SMS Type",
|
||||
octopushTypePremium: "Premium (Fast - recommended for alerting)",
|
||||
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)",
|
||||
checkPrice: "Check {0} prices:",
|
||||
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
|
||||
"Check octopush prices": "Check octopush prices {0}.",
|
||||
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
|
||||
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
|
||||
|
@ -281,5 +288,21 @@ export default {
|
|||
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
|
||||
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
"Feishu WebHookUrl": "Feishu WebHookUrl",
|
||||
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
|
||||
"Internal Room Id": "Internal Room Id",
|
||||
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
|
||||
// End notification form
|
||||
Method: "Method",
|
||||
Body: "Body",
|
||||
Headers: "Headers",
|
||||
PushUrl: "Push URL",
|
||||
HeadersInvalidFormat: "The request headers are not valid JSON: ",
|
||||
BodyInvalidFormat: "The request body is not valid JSON: ",
|
||||
"Monitor History": "Monitor History:",
|
||||
clearDataOlderThan: "Keep monitor history data for {0} days.",
|
||||
records: "records",
|
||||
"One record": "One record",
|
||||
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records",
|
||||
steamApiKeyDescription: "For monitoring a Steam Gameserver you need a steam Web-API key. You can register your api key here: ",
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ export default {
|
|||
passwordNotMatchMsg: "La contraseña repetida no coincide.",
|
||||
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
|
||||
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
|
||||
pauseDashboardHome: "Pausar",
|
||||
pauseDashboardHome: "Pausado",
|
||||
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
|
||||
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
|
||||
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
|
||||
|
@ -32,7 +32,7 @@ export default {
|
|||
Down: "Caído",
|
||||
Pending: "Pendiente",
|
||||
Unknown: "Desconocido",
|
||||
Pause: "Pausa",
|
||||
Pause: "Pausar",
|
||||
Name: "Nombre",
|
||||
Status: "Estado",
|
||||
DateTime: "Fecha y Hora",
|
||||
|
@ -198,4 +198,9 @@ export default {
|
|||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Monitor History": "Historial de monitor:",
|
||||
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
|
||||
records: "registros",
|
||||
"One record": "Un registro",
|
||||
"Showing {from} to {to} of {count} records": "Mostrando desde {from} a {to} de {count} registros",
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ export default {
|
|||
passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
|
||||
notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.",
|
||||
keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.",
|
||||
pauseDashboardHome: "Éléments mis en pause",
|
||||
pauseDashboardHome: "En pause",
|
||||
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
|
||||
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
|
||||
resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
|
||||
|
@ -54,7 +54,7 @@ export default {
|
|||
Delete: "Supprimer",
|
||||
Current: "Actuellement",
|
||||
Uptime: "Uptime",
|
||||
"Cert Exp.": "Certificat expiré",
|
||||
"Cert Exp.": "Expiration SSL",
|
||||
days: "jours",
|
||||
day: "jour",
|
||||
"-day": "-jours",
|
||||
|
@ -185,7 +185,7 @@ export default {
|
|||
"Required": "Requis",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
"You can get a token from": "Vous pouvez obtenir un token depuis",
|
||||
wayToGetTelegramToken: "Vous pouvez obtenir un token depuis {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "Supporte les messages privés / en groupe / l'ID du salon",
|
||||
wayToGetTelegramChatID: "Vous pouvez obtenir l'ID du chat en envoyant un message avec le bot puis en récupérant l'URL pour voir l'ID du salon :",
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
export default {
|
||||
languageName: "Indonesia",
|
||||
languageName: "Bahasa Indonesia (Indonesian)",
|
||||
checkEverySecond: "Cek Setiap {0} detik.",
|
||||
retryCheckEverySecond: "Coba lagi setiap {0} detik.",
|
||||
retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim",
|
||||
ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS",
|
||||
upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
|
||||
maxRedirectDescription: "Jumlah maksimum pengalihan untuk diikuti. Setel ke 0 untuk menonaktifkan pengalihan.",
|
||||
acceptedStatusCodesDescription: "Pilih kode status yang dianggap sebagai respons yang berhasil.",
|
||||
acceptedStatusCodesDescription: "Pilih kode status yang dianggap sebagai tanggapan yang berhasil.",
|
||||
passwordNotMatchMsg: "Sandi kedua tidak cocok.",
|
||||
notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.",
|
||||
keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
|
||||
pauseDashboardHome: "Jeda",
|
||||
deleteMonitorMsg: "Apakah anda mau menghapus monitor ini?",
|
||||
deleteNotificationMsg: "Apakah anda mau menghapus notifikasi ini untuk semua monitor?",
|
||||
resoverserverDescription: "Cloudflare adalah server default, Anda dapat mengubah server resolver kapan saja.",
|
||||
rrtypeDescription: "Pilih RR-Type yang mau anda monitor",
|
||||
pauseMonitorMsg: "Apakah anda yakin mau menjeda?",
|
||||
enableDefaultNotificationDescription: "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara default. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.",
|
||||
clearEventsMsg: "Apakah anda yakin mau menghapus semua event di monitor ini?",
|
||||
clearHeartbeatsMsg: "Apakah anda yakin mau menghapus semua heartbeats di monitor ini?",
|
||||
confirmClearStatisticsMsg: "Apakah anda yakin mau menghapus semua statistik?",
|
||||
deleteMonitorMsg: "Apakah Anda mau menghapus monitor ini?",
|
||||
deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi ini untuk semua monitor?",
|
||||
resoverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
|
||||
rrtypeDescription: "Pilih RR-Type yang mau Anda monitor",
|
||||
pauseMonitorMsg: "Apakah Anda yakin mau menjeda?",
|
||||
enableDefaultNotificationDescription: "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara bawaan. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.",
|
||||
clearEventsMsg: "Apakah Anda yakin mau menghapus semua event di monitor ini?",
|
||||
clearHeartbeatsMsg: "Apakah Anda yakin mau menghapus semua heartbeats di monitor ini?",
|
||||
confirmClearStatisticsMsg: "Apakah Anda yakin mau menghapus semua statistik?",
|
||||
importHandleDescription: "Pilih 'Lewati yang ada' jika Anda ingin melewati setiap monitor atau notifikasi dengan nama yang sama. 'Timpa' akan menghapus setiap monitor dan notifikasi yang ada.",
|
||||
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
|
||||
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
|
||||
tokenValidSettingsMsg: "Tokennya valid! Anda sekarang dapat menyimpan pengaturan 2FA.",
|
||||
tokenValidSettingsMsg: "Tokennya benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
|
||||
confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
|
||||
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
|
||||
Settings: "Pengaturan",
|
||||
Dashboard: "Dashboard",
|
||||
"New Update": "Update Baru",
|
||||
Dashboard: "Dasbor",
|
||||
"New Update": "Pembaruan Baru",
|
||||
Language: "Bahasa",
|
||||
Appearance: "Tampilan",
|
||||
Theme: "Tema",
|
||||
General: "General",
|
||||
General: "Umum",
|
||||
Version: "Versi",
|
||||
"Check Update On GitHub": "Cek Update di GitHub",
|
||||
List: "List",
|
||||
"Check Update On GitHub": "Cek Pembaruan di GitHub",
|
||||
List: "Daftar",
|
||||
Add: "Tambah",
|
||||
"Add New Monitor": "Tambah Monitor Baru",
|
||||
"Quick Stats": "Statistik Cepat",
|
||||
"Quick Stats": "Statistik",
|
||||
Up: "Aktif",
|
||||
Down: "Tidak Aktif",
|
||||
Pending: "Tertunda",
|
||||
|
@ -48,9 +48,9 @@ export default {
|
|||
Status: "Status",
|
||||
DateTime: "Tanggal Waktu",
|
||||
Message: "Pesan",
|
||||
"No important events": "Tidak ada Event penting",
|
||||
Resume: "Melanjutkan",
|
||||
Edit: "Rubah",
|
||||
"No important events": "Tidak ada peristiwa penting",
|
||||
Resume: "Lanjut",
|
||||
Edit: "Ubah",
|
||||
Delete: "Hapus",
|
||||
Current: "Saat ini",
|
||||
Uptime: "Waktu aktif",
|
||||
|
@ -60,20 +60,20 @@ export default {
|
|||
"-day": "-hari",
|
||||
hour: "Jam",
|
||||
"-hour": "-Jam",
|
||||
Response: "Respon",
|
||||
Response: "Tanggapan",
|
||||
Ping: "Ping",
|
||||
"Monitor Type": "Tipe Monitor",
|
||||
Keyword: "Keyword",
|
||||
"Friendly Name": "Friendly Name",
|
||||
"Friendly Name": "Nama yang Ramah",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Interval Heartbeat ",
|
||||
Retries: "Retries",
|
||||
"Heartbeat Retry Interval": "Interval Heartbeat Mencoba kembali ",
|
||||
Advanced: "Advanced",
|
||||
"Heartbeat Interval": "Jarak Waktu Heartbeat ",
|
||||
Retries: "Coba lagi",
|
||||
"Heartbeat Retry Interval": "Jarak Waktu Heartbeat Mencoba kembali ",
|
||||
Advanced: "Tingkat Lanjut",
|
||||
"Upside Down Mode": "Mode Terbalik",
|
||||
"Max. Redirects": "Maksimal Redirect/Pengalihan",
|
||||
"Max. Redirects": "Maksimal Pengalihan",
|
||||
"Accepted Status Codes": "Kode Status yang Diterima",
|
||||
Save: "Simpan",
|
||||
Notifications: "Notifikasi",
|
||||
|
@ -81,25 +81,25 @@ export default {
|
|||
"Setup Notification": "Setel Notifikasi",
|
||||
Light: "Terang",
|
||||
Dark: "Gelap",
|
||||
Auto: "Automatis",
|
||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
||||
Auto: "Otomatis",
|
||||
"Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
|
||||
Normal: "Normal",
|
||||
Bottom: "Bawah",
|
||||
None: "Tidak ada",
|
||||
Timezone: "Zona Waktu",
|
||||
"Search Engine Visibility": "Visibilitas Mesin Pencari",
|
||||
"Allow indexing": "Mengizinkan untuk diindex",
|
||||
"Discourage search engines from indexing site": "Mencegah mesin pencari untuk mengindex site",
|
||||
"Discourage search engines from indexing site": "Mencegah mesin pencari untuk mengindex situs",
|
||||
"Change Password": "Ganti Sandi",
|
||||
"Current Password": "Sandi Lama",
|
||||
"New Password": "Sandi Baru",
|
||||
"Repeat New Password": "Ulangi Sandi Baru",
|
||||
"Update Password": "Perbarui Kata Sandi",
|
||||
"Disable Auth": "Nonaktifkan auth",
|
||||
"Enable Auth": "Aktifkan Auth",
|
||||
"Disable Auth": "Nonaktifkan Autentikasi",
|
||||
"Enable Auth": "Aktifkan Autentikasi",
|
||||
Logout: "Keluar",
|
||||
Leave: "Pergi",
|
||||
"I understand, please disable": "Saya mengerti, silahkan dinonaktifkan",
|
||||
"I understand, please disable": "Saya mengerti, silakan dinonaktifkan",
|
||||
Confirm: "Konfirmasi",
|
||||
Yes: "Ya",
|
||||
No: "Tidak",
|
||||
|
@ -107,35 +107,35 @@ export default {
|
|||
Password: "Sandi",
|
||||
"Remember me": "Ingat saya",
|
||||
Login: "Masuk",
|
||||
"No Monitors, please": "Tidak ada monitor, silahkan",
|
||||
"add one": "tambah baru",
|
||||
"No Monitors, please": "Tidak ada monitor, silakan",
|
||||
"add one": "tambahkan satu",
|
||||
"Notification Type": "Tipe Notifikasi",
|
||||
Email: "Email",
|
||||
Test: "Test",
|
||||
"Certificate Info": "Info Sertifikasi ",
|
||||
Email: "Surel",
|
||||
Test: "Tes",
|
||||
"Certificate Info": "Info Sertifikasi",
|
||||
"Resolver Server": "Resolver Server",
|
||||
"Resource Record Type": "Resource Record Type",
|
||||
"Last Result": "Hasil Terakhir",
|
||||
"Create your admin account": "Buat admin akun anda",
|
||||
"Create your admin account": "Buat admin akun Anda",
|
||||
"Repeat Password": "Ulangi Sandi",
|
||||
"Import Backup": "Impor Backup",
|
||||
"Export Backup": "Expor Backup",
|
||||
"Import Backup": "Impor Cadangan",
|
||||
"Export Backup": "Expor Cadangan",
|
||||
Export: "Expor",
|
||||
Import: "Impor",
|
||||
respTime: "Tanggapan. Waktu (milidetik)",
|
||||
notAvailableShort: "N/A",
|
||||
"Default enabled": "Default diaktifkan",
|
||||
"Default enabled": "Bawaan diaktifkan",
|
||||
"Apply on all existing monitors": "Terapkan pada semua monitor yang ada",
|
||||
Create: "Buat",
|
||||
"Clear Data": "Bersihkan Data",
|
||||
Events: "Event",
|
||||
Events: "Peristiwa",
|
||||
Heartbeats: "Heartbeats",
|
||||
"Auto Get": "Auto Get",
|
||||
backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam file JSON.",
|
||||
backupDescription2: "Catatan: Data sejarah dan event tidak disertakan.",
|
||||
backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam file ekspor, harap simpan dengan hati-hati.",
|
||||
alertNoFile: "Silakan pilih file untuk diimpor.",
|
||||
alertWrongFileType: "Silakan pilih file JSON .",
|
||||
"Auto Get": "Ambil Otomatis",
|
||||
backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam berkas JSON.",
|
||||
backupDescription2: "Catatan: Data sejarah dan peristiwa tidak disertakan.",
|
||||
backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam berkas ekspor, harap simpan dengan hati-hati.",
|
||||
alertNoFile: "Silakan pilih berkas untuk diimpor.",
|
||||
alertWrongFileType: "Silakan pilih berkas JSON.",
|
||||
"Clear all statistics": "Hapus semua statistik",
|
||||
"Skip existing": "Lewati yang ada",
|
||||
Overwrite: "Timpa",
|
||||
|
@ -145,29 +145,29 @@ export default {
|
|||
"Setup 2FA": "Pengaturan 2FA",
|
||||
"Enable 2FA": "Aktifkan 2FA",
|
||||
"Disable 2FA": "Nonaktifkan 2FA",
|
||||
"2FA Settings": "Settings 2FA",
|
||||
"Two Factor Authentication": "Otentikasi Dua Faktor",
|
||||
"2FA Settings": "Pengaturan 2FA",
|
||||
"Two Factor Authentication": "Autentikasi Dua Faktor",
|
||||
Active: "Aktif",
|
||||
Inactive: "Tidak Aktif",
|
||||
Token: "Token",
|
||||
"Show URI": "Lihat URI",
|
||||
Tags: "Tag",
|
||||
Tags: "Tanda",
|
||||
"Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
|
||||
"Tag with this name already exist.": "Tag dengan nama ini sudah ada.",
|
||||
"Tag with this value already exist.": "Tag dengan nilai ini sudah ada.",
|
||||
"Tag with this name already exist.": "Tanda dengan nama ini sudah ada.",
|
||||
"Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
|
||||
color: "warna",
|
||||
"value (optional)": "nilai (harus diisi)",
|
||||
Gray: "Abu Abu",
|
||||
Gray: "Abu-abu",
|
||||
Red: "Merah",
|
||||
Orange: "Oranye",
|
||||
Orange: "Jingga",
|
||||
Green: "Hijau",
|
||||
Blue: "Biru",
|
||||
Indigo: "Indigo",
|
||||
Indigo: "Biru Tua",
|
||||
Purple: "Ungu",
|
||||
Pink: "Merah Muda",
|
||||
"Search...": "Cari...",
|
||||
"Avg. Ping": "Rata-rata. Ping",
|
||||
"Avg. Response": "Rata-rata. Respon",
|
||||
"Avg. Ping": "Rata-rata Ping",
|
||||
"Avg. Response": "Rata-rata Tanggapan",
|
||||
"Entry Page": "Halaman Masuk",
|
||||
statusPageNothing: "Tidak ada di sini, silakan tambahkan grup atau monitor.",
|
||||
"No Services": "Tidak ada Layanan",
|
||||
|
@ -177,7 +177,7 @@ export default {
|
|||
"Add Group": "Tambah Grup",
|
||||
"Add a monitor": "Tambah monitor",
|
||||
"Edit Status Page": "Edit Halaman Status",
|
||||
"Go to Dashboard": "Lihat Dashboard",
|
||||
"Go to Dashboard": "Pergi ke Dasbor",
|
||||
"Status Page": "Halaman Status",
|
||||
// Start notification form
|
||||
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
||||
|
@ -194,22 +194,22 @@ export default {
|
|||
"webhook": "Webhook",
|
||||
"Post URL": "Post URL",
|
||||
"Content Type": "Tipe konten",
|
||||
webhookJsonDesc: "{0} bagus untuk server http modern seperti express.js",
|
||||
webhookJsonDesc: "{0} bagus untuk peladen http modern seperti express.js",
|
||||
webhookFormDataDesc: "{multipart} bagus untuk PHP, Anda hanya perlu mengurai json dengan {decodeFunction}",
|
||||
"smtp": "Email (SMTP)",
|
||||
"smtp": "Surel (SMTP)",
|
||||
secureOptionNone: "None / STARTTLS (25, 587)",
|
||||
secureOptionTLS: "TLS (465)",
|
||||
"Ignore TLS Error": "Ignore TLS Error",
|
||||
"From Email": "From Email",
|
||||
"To Email": "To Email",
|
||||
"Ignore TLS Error": "Abaikan Kesalahan TLS",
|
||||
"From Email": "Dari Surel",
|
||||
"To Email": "Ke Surel",
|
||||
smtpCC: "CC",
|
||||
smtpBCC: "BCC",
|
||||
"discord": "Discord",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook",
|
||||
"Bot Display Name": "Nama Bot",
|
||||
"Prefix Custom Message": "Prefix Pesan",
|
||||
"Hello @everyone is...": "Hallo {'@'}everyone is...",
|
||||
"Prefix Custom Message": "Awalan Pesan",
|
||||
"Hello @everyone is...": "Halo {'@'}everyone is...",
|
||||
"teams": "Microsoft Teams",
|
||||
"Webhook URL": "Webhook URL",
|
||||
wayToGetTeamsURL: "Anda dapat mempelajari cara membuat url webhook {0}.",
|
||||
|
@ -221,16 +221,16 @@ export default {
|
|||
signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!",
|
||||
"gotify": "Gotify",
|
||||
"Application Token": "Token Aplikasi",
|
||||
"Server URL": "Server URL",
|
||||
"Server URL": "URL Peladen",
|
||||
"Priority": "Prioritas",
|
||||
"slack": "Slack",
|
||||
"Icon Emoji": "Icon Emoji",
|
||||
"Channel Name": "Nama Channel",
|
||||
"Icon Emoji": "Ikon Emoji",
|
||||
"Channel Name": "Nama Saluran",
|
||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||
aboutWebhooks: "Info lain tentang webhook: {0}",
|
||||
aboutChannelName: "Masukan nama channel di {0} Kolom Nama Channel jika Anda ingin melewati channel webhook. Contoh: #other-channel",
|
||||
aboutKumaURL: "Jika Anda membiarkan bidang URL Uptime Kuma kosong, itu akan menjadi default ke halaman Project Github.",
|
||||
emojiCheatSheet: "Emoji cheat sheet: {0}",
|
||||
aboutChannelName: "Masukan nama saluran di {0} Kolom Nama Saluran jika Anda ingin melewati saluran webhook. Contoh: #saluran-lain",
|
||||
aboutKumaURL: "Jika Anda membiarkan bidang URL Uptime Kuma kosong, itu akan menjadi bawaan ke halaman Proyek Github.",
|
||||
emojiCheatSheet: "Lembar contekan emoji: {0}",
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
|
@ -246,7 +246,7 @@ export default {
|
|||
"Message Title": "Judul Pesan",
|
||||
"Notification Sound": "Suara Nofifikasi",
|
||||
"More info on:": "Info lebih lanjut tentang: {0}",
|
||||
pushoverDesc1: "Prioritas darurat (2) memiliki batas waktu default 30 detik antara percobaan ulang dan akan kadaluwarsa setelah 1 jam.",
|
||||
pushoverDesc1: "Prioritas darurat (2) memiliki batas waktu bawaan 30 detik antara percobaan ulang dan akan kadaluwarsa setelah 1 jam.",
|
||||
pushoverDesc2: "Jika Anda ingin mengirim pemberitahuan ke perangkat yang berbeda, isi kolom Perangkat.",
|
||||
"SMS Type": "Tipe SMS",
|
||||
octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)",
|
||||
|
@ -262,24 +262,24 @@ export default {
|
|||
"Read more": "Baca lebih lajut",
|
||||
appriseInstalled: "Apprise diinstall.",
|
||||
appriseNotInstalled: "Apprise tidak diinstall. {0}",
|
||||
"Access Token": "Access Token",
|
||||
"Channel access token": "Channel access token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
lineDevConsoleTo: "Line Developers Console - {0}",
|
||||
"Basic Settings": "Pengaturan dasar",
|
||||
"User ID": "User ID",
|
||||
"Access Token": "Token Akses",
|
||||
"Channel access token": "Token akses saluran",
|
||||
"Line Developers Console": "Konsol Pengembang Line",
|
||||
lineDevConsoleTo: "Konsol Pengembang Line - {0}",
|
||||
"Basic Settings": "Pengaturan Dasar",
|
||||
"User ID": "ID User",
|
||||
"Messaging API": "Messaging API",
|
||||
wayToGetLineChannelToken: "Pertama akses {0}, buat penyedia dan saluran (Messaging API), lalu Anda bisa mendapatkan token akses saluran dan id pengguna dari item menu yang disebutkan di atas.",
|
||||
"Icon URL": "Icon URL",
|
||||
aboutIconURL: "Anda dapat memberikan tautan ke gambar di \"Icon URL\" untuk mengganti gambar profil default. Tidak akan digunakan jika Ikon Emoji diset.",
|
||||
aboutMattermostChannelName: "Anda dapat mengganti channel default tujuan posting webhook dengan memasukkan nama channel ke dalam Kolom \"Channel Name\". Ini perlu diaktifkan di pengaturan webhook Mattermost. contoh: #other-channel",
|
||||
aboutIconURL: "Anda dapat memberikan tautan ke gambar di \"Icon URL\" untuk mengganti gambar profil bawaan. Tidak akan digunakan jika Ikon Emoji diset.",
|
||||
aboutMattermostChannelName: "Anda dapat mengganti saluran bawaan tujuan posting webhook dengan memasukkan nama saluran ke dalam Kolom \"Channel Name\". Ini perlu diaktifkan di pengaturan webhook Mattermost. contoh: #other-channel",
|
||||
"matrix": "Matrix",
|
||||
promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
|
||||
promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
|
||||
promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diandalkan untuk peringatan.",
|
||||
promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
|
||||
promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diAndalkan untuk peringatan.",
|
||||
promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diAndalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
|
||||
promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
|
||||
promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu default: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
|
||||
promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
|
||||
"Feishu WebHookUrl": "Feishu WebHookUrl",
|
||||
// End notification form
|
||||
};
|
||||
|
|
|
@ -184,7 +184,7 @@ export default {
|
|||
Required: "필수",
|
||||
telegram: "Telegram",
|
||||
"Bot Token": "봇 토큰",
|
||||
"You can get a token from": "토큰은 여기서 얻을 수 있어요:",
|
||||
wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
|
||||
"Chat ID": "채팅 ID",
|
||||
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
||||
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
||||
|
|
|
@ -185,7 +185,7 @@ export default {
|
|||
"Required": "Obligatorisk",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Bot Token",
|
||||
"You can get a token from": "Du kan få et token fra",
|
||||
wayToGetTelegramToken: "Du kan få et token fra {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
|
||||
wayToGetTelegramChatID: "Du kan få chat-ID-en din ved å sende meldingen til boten og gå til denne nettadressen for å se chat_id:",
|
||||
|
|
|
@ -198,4 +198,10 @@ export default {
|
|||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
Method: "Methode",
|
||||
Body: "Body",
|
||||
Headers: "Headers",
|
||||
PushUrl: "Push URL",
|
||||
HeadersInvalidFormat: "The request headers is geen geldige JSON: ",
|
||||
BodyInvalidFormat: "De request body is geen geldige JSON: "
|
||||
};
|
||||
|
|
|
@ -185,7 +185,7 @@ export default {
|
|||
"Required": "Wymagane",
|
||||
"telegram": "Telegram",
|
||||
"Bot Token": "Token Bota",
|
||||
"You can get a token from": "Token można uzyskać z",
|
||||
wayToGetTelegramToken: "Token można uzyskać z {0}.",
|
||||
"Chat ID": "Identyfikator Czatu",
|
||||
supportTelegramChatID: "Czat wsprarcia technicznego / Bezpośrednia Rozmowa / Czat Grupowy",
|
||||
wayToGetTelegramChatID: "Możesz uzyskać swój identyfikator czatu, wysyłając wiadomość do bota i przechodząc pod ten adres URL, aby wyświetlić identyfikator czatu:",
|
||||
|
|
|
@ -116,39 +116,39 @@ export default {
|
|||
Events: "Olaylar",
|
||||
Heartbeats: "Sağlık Durumları",
|
||||
"Auto Get": "Otomatik Al",
|
||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||
"Import Backup": "Import Backup",
|
||||
"Export Backup": "Export Backup",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
"Default enabled": "Default enabled",
|
||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||
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.",
|
||||
"Clear all statistics": "Clear all Statistics",
|
||||
"Skip existing": "Skip existing",
|
||||
Overwrite: "Overwrite",
|
||||
Options: "Options",
|
||||
"Keep both": "Keep both",
|
||||
"Verify Token": "Verify Token",
|
||||
"Setup 2FA": "Setup 2FA",
|
||||
"Enable 2FA": "Enable 2FA",
|
||||
"Disable 2FA": "Disable 2FA",
|
||||
"2FA Settings": "2FA Settings",
|
||||
"Two Factor Authentication": "Two Factor Authentication",
|
||||
Active: "Active",
|
||||
Inactive: "Inactive",
|
||||
retryCheckEverySecond: "{0} Saniyede bir dene.",
|
||||
enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
|
||||
importHandleDescription: "Aynı isimdeki bütün servisleri ve bildirimleri atlamak için 'Var olanı atla' seçiniz. 'Üzerine yaz' var olan bütün servisleri ve bildirimleri silecektir. ",
|
||||
confirmImportMsg: "Yedeği içeri aktarmak istediğinize emin misiniz? Lütfen doğru içeri aktarma seçeneğini seçtiğinizden emin olunuz. ",
|
||||
twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
|
||||
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
||||
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
||||
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
||||
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
|
||||
"Import Backup": "Yedeği içe aktar",
|
||||
"Export Backup": "Yedeği dışa aktar",
|
||||
Export: "Dışa aktar",
|
||||
Import: "İçe aktar",
|
||||
"Default enabled": "Varsayılan etkinleştirilmiş",
|
||||
"Apply on all existing monitors": "Var olan bütün servislere uygula",
|
||||
backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.",
|
||||
backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.",
|
||||
backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.",
|
||||
alertNoFile: "İçeri aktarmak için bir dosya seçiniz.",
|
||||
alertWrongFileType: "Lütfen bir JSON dosyası seçiniz.",
|
||||
"Clear all statistics": "Bütün istatistikleri temizle",
|
||||
"Skip existing": "Var olanı atla",
|
||||
Overwrite: "Üzerine yaz",
|
||||
Options: "Seçenekler",
|
||||
"Keep both": "İkisini sakla",
|
||||
"Verify Token": "Tokeni doğrula",
|
||||
"Setup 2FA": "2FA Kur",
|
||||
"Enable 2FA": "2FA Etkinleştir",
|
||||
"Disable 2FA": "2FA Devre dışı bırak",
|
||||
"2FA Settings": "2FA Ayarları",
|
||||
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||
Active: "Aktif",
|
||||
Inactive: "İnaktif",
|
||||
Token: "Token",
|
||||
"Show URI": "Show URI",
|
||||
Tags: "Tags",
|
||||
|
|
|
@ -22,7 +22,8 @@ export default {
|
|||
Appearance: "外观设置",
|
||||
Theme: "主题",
|
||||
General: "基本设置",
|
||||
Version: "Version",
|
||||
"Primary Base URL": "站点地址(URL)",
|
||||
Version: "版本",
|
||||
"Check Update On GitHub": "检查更新",
|
||||
List: "列表",
|
||||
Add: "添加",
|
||||
|
@ -43,7 +44,7 @@ export default {
|
|||
Delete: "删除",
|
||||
Current: "当前",
|
||||
Uptime: "可用率",
|
||||
"Cert Exp.": "证书过期",
|
||||
"Cert Exp.": "证书有效期",
|
||||
days: "天",
|
||||
day: "天",
|
||||
"-day": " 天",
|
||||
|
@ -63,6 +64,9 @@ export default {
|
|||
"Upside Down Mode": "反向监控",
|
||||
"Max. Redirects": "重定向次数",
|
||||
"Accepted Status Codes": "有效状态码",
|
||||
"Push URL": "推送链接",
|
||||
needPushEvery: "你需要每 {0} 秒调用一次。",
|
||||
pushOptionalParams: "可选参数:{0}",
|
||||
Save: "保存",
|
||||
Notifications: "消息通知",
|
||||
"Not available, please setup.": "无可用通道,请先设置",
|
||||
|
@ -103,10 +107,10 @@ export default {
|
|||
"Certificate Info": "证书信息",
|
||||
"Resolver Server": "解析服务器",
|
||||
"Resource Record Type": "资源记录类型",
|
||||
"Last Result": "Last Result",
|
||||
"Last Result": "最后结果",
|
||||
"Create your admin account": "创建管理员账号",
|
||||
"Repeat Password": "重复密码",
|
||||
respTime: "Resp. Time (ms)",
|
||||
respTime: "响应时间(毫秒)",
|
||||
notAvailableShort: "N/A",
|
||||
Create: "创建",
|
||||
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
|
||||
|
@ -126,21 +130,21 @@ export default {
|
|||
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
||||
alertNoFile: "请选择一个文件导入",
|
||||
alertWrongFileType: "请选择一个 JSON 格式的文件",
|
||||
twoFAVerifyLabel: "请输入Token以验证2FA(二次验证)是否正常工作",
|
||||
tokenValidSettingsMsg: "Token有效!您现在可以保存2FA(二次验证)设置",
|
||||
confirmEnableTwoFAMsg: "确定要启用2FA(二次验证)吗?",
|
||||
confirmDisableTwoFAMsg: "确定要禁用2FA(二次验证)吗?",
|
||||
twoFAVerifyLabel: "请输入Token以验证 2FA(二次验证)是否正常工作",
|
||||
tokenValidSettingsMsg: "Token有效!您现在可以保存 2FA(二次验证)设置",
|
||||
confirmEnableTwoFAMsg: "确定要启用 2FA(二次验证)吗?",
|
||||
confirmDisableTwoFAMsg: "确定要禁用 2FA(二次验证)吗?",
|
||||
"Apply on all existing monitors": "应用到所有监控项",
|
||||
"Verify Token": "验证Token",
|
||||
"Setup 2FA": "设置2FA",
|
||||
"Enable 2FA": "启用2FA",
|
||||
"Disable 2FA": "禁用2FA",
|
||||
"2FA Settings": "2FA设置",
|
||||
"Verify Token": "验证 Token",
|
||||
"Setup 2FA": "设置 2FA",
|
||||
"Enable 2FA": "启用 2FA",
|
||||
"Disable 2FA": "禁用 2FA",
|
||||
"2FA Settings": "2FA 设置",
|
||||
"Two Factor Authentication": "双因素认证",
|
||||
Active: "有效",
|
||||
Inactive: "无效",
|
||||
Active: "生效",
|
||||
Inactive: "未生效",
|
||||
Token: "Token",
|
||||
"Show URI": "显示URI",
|
||||
"Show URI": "显示链接",
|
||||
"Clear all statistics": "清除所有统计数据",
|
||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
|
||||
|
@ -167,7 +171,7 @@ export default {
|
|||
Purple: "紫色",
|
||||
Pink: "粉色",
|
||||
"Search...": "搜索...",
|
||||
"Avg. Ping": "平均Ping",
|
||||
"Avg. Ping": "平均 Ping",
|
||||
"Avg. Response": "平均响应",
|
||||
"Entry Page": "入口页面",
|
||||
statusPageNothing: "这里什么也没有,请添加一个分组或一个监控项。",
|
||||
|
@ -182,7 +186,7 @@ export default {
|
|||
"Status Page": "状态页",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "Email (SMTP)",
|
||||
smtp: "电子邮件(SMTP)",
|
||||
discord: "Discord",
|
||||
teams: "Microsoft Teams",
|
||||
signal: "Signal",
|
||||
|
@ -194,9 +198,97 @@ export default {
|
|||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
apprise: "Apprise (Support 50+ Notification services)",
|
||||
apprise: "Apprise (支持50+种通知服务)",
|
||||
pushbullet: "Pushbullet",
|
||||
line: "Line Messenger",
|
||||
mattermost: "Mattermost",
|
||||
"Feishu WebHookUrl": "飞书 WebHook 地址",
|
||||
defaultNotificationName: "{notification} 通知({number})",
|
||||
here: "这里",
|
||||
Required: "必填",
|
||||
"Bot Token": "Bot Token",
|
||||
wayToGetTelegramToken: "你可以从 {0} 获取 Token。",
|
||||
"Chat ID": "Chat ID",
|
||||
supportTelegramChatID: "支持对话/群组/频道的 ID",
|
||||
wayToGetTelegramChatID: "你可以发送一条消息给你的机器人然后到下面的链接来查看你的 chat_id:",
|
||||
"YOUR BOT TOKEN HERE": "这里替换成你的 BOT TOKEN",
|
||||
chatIDNotFound: "没有找到 Chat ID,请先给你的机器人发送一条消息。",
|
||||
"Post URL": "目标链接",
|
||||
"Content Type": "Content Type",
|
||||
webhookJsonDesc: "{0} 适合现代的服务,比如 express.js",
|
||||
webhookFormDataDesc: "{multipart} 适合PHP,解码使用 {decodeFunction}",
|
||||
secureOptionNone: "无 / STARTTLS(25,587)",
|
||||
secureOptionTLS: "TLS(465)",
|
||||
"Ignore TLS Error": "忽略 TLS 错误",
|
||||
"From Email": "发信人",
|
||||
"To Email": "收信人",
|
||||
smtpCC: "抄送",
|
||||
smtpBCC: "密送",
|
||||
"Discord Webhook URL": "Discord Webhook 链接",
|
||||
wayToGetDiscordURL: "获取方式:服务器设置 -> 整合 -> 创建 Webhook",
|
||||
"Bot Display Name": "机器人显示名称",
|
||||
"Prefix Custom Message": "自定义消息前缀",
|
||||
"Hello @everyone is...": "{'@'}所有人,……",
|
||||
"Webhook URL": "Webhook 链接",
|
||||
wayToGetTeamsURL: "你可以在 {0} 获取 Webhook 链接。",
|
||||
Number: "号码",
|
||||
Recipients: "收件人",
|
||||
needSignalAPI: "你需要有一个带 REST API 的 Signal 客户端。",
|
||||
wayToCheckSignalURL: "你可以通过下面的链接来了解如何设置:",
|
||||
signalImportant: "重要:你不能混合设定收件人的分组和号码!",
|
||||
"Application Token": "Application Token",
|
||||
"Server URL": "服务器链接",
|
||||
Priority: "优先级",
|
||||
"Icon Emoji": "Emoji 图标",
|
||||
"Channel Name": "频道名称",
|
||||
"Uptime Kuma URL": "Uptime Kuma 链接",
|
||||
aboutWebhooks: "关于 Webhook 的更多信息:{0}",
|
||||
aboutChannelName: "如果你想绕过 Webhook 设定的频道,请在设定 {0} 的频道名称字段为你想要的频道。例:#other-channel",
|
||||
aboutKumaURL: "如果保留 Uptime Kuma 链接为空,将会默认指向项目的 Github 页面。",
|
||||
emojiCheatSheet: "Emoji 参考表:{0}",
|
||||
"User Key": "User Key",
|
||||
Device: "设备",
|
||||
"Message Title": "消息标题",
|
||||
"Notification Sound": "通知铃声",
|
||||
"More info on:": "更多信息:{0}",
|
||||
pushoverDesc1: "紧急优先级(2)会在一小时内每30秒重试一次。",
|
||||
pushoverDesc2: "如果你想发送通知给不同的设备,请填写“设备”字段。",
|
||||
"SMS Type": "短信类型",
|
||||
octopushTypePremium: "Premium(快 - 推荐用于警报)",
|
||||
octopushTypeLowCost: "Low Cost(慢 - 有时会被运营商屏蔽)",
|
||||
"Check octopush prices": "查看 Octopush 的价格 {0}。",
|
||||
octopushPhoneNumber: "电话号码(国际格式,例:+33612345678)",
|
||||
octopushSMSSender: "短信发送名称:3-11位大小写字母、数字和空格(a-zA-Z0-9)",
|
||||
"LunaSea Device ID": "LunaSea 设备 ID",
|
||||
"Apprise URL": "Apprise 链接",
|
||||
"Example:": "例:{0}",
|
||||
"Read more:": "了解更多:{0}",
|
||||
"Status:": "状态:{0}",
|
||||
"Read more": "了解更多",
|
||||
appriseInstalled: "Apprise 已安装",
|
||||
appriseNotInstalled: "Apprise 未安装。{0}",
|
||||
"Access Token": "Access Token",
|
||||
"Channel access token": "频道 access token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
lineDevConsoleTo: "Line Developers Console - {0}",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"User ID": "User ID",
|
||||
"Messaging API": "Messaging API",
|
||||
wayToGetLineChannelToken: "首先访问 {0},创建一个提供者和频道(Messaging API),然后你就可以从上面提到的地方获取频道的 access token 和用户 ID。",
|
||||
"Icon URL": "图标链接",
|
||||
aboutIconURL: "你可以在“Icon URL”中提供一个图片地址来覆盖默认的资料图片。如果设置了 Emoji 图标此字段会被忽略。",
|
||||
aboutMattermostChannelName: "如果你想覆盖 Webhook 设定的频道,请在“频道名称”字段为你想要的频道。这需要在 Mattermost 的 Webhook 设定中启用。例:#other-channel",
|
||||
matrix: "Matrix",
|
||||
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收件人。",
|
||||
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收件人设备上。仅限波兰地区的收件人。",
|
||||
promosmsTypeFull: "SMS FULL - 高等级,你可以使用你自己的发件人名称(你需要先注册一个). 对于警报来说更可靠。",
|
||||
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(越两倍 SMS FULL 等级的价格)。",
|
||||
promosmsPhoneNumber: "电话号码(波兰地区收件人可以不填区号)",
|
||||
promosmsSMSSender: "短信发件人名称:已注册的名称或以下默认值之一:InfoSMS,SMS Info,MaxSMS,INFO,SMS",
|
||||
checkPrice: "查看 {0} 的价格:",
|
||||
octopushLegacyHint: "你是否在使用旧版本的 Octopush(2011-2020)?",
|
||||
matrixHomeserverURL: "服务器链接(开头带 http(s):// 和可能的需要的端口号)",
|
||||
"Internal Room Id": "Internal Room Id",
|
||||
matrixDesc1: "你可以在 Matrix 客户端房间设置的高级选项找到 Internal Room Id。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
|
||||
matrixDesc2: "请不要使用你自己的 Access Token,这将开放你所有的账户权限和你加入的房间权限。你可以创建一个新的用户并邀请它至你允许的的房间中。你可以运行以下命令来获取 Access Token:{0}",
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ $t(status.text) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
<option value="push">
|
||||
Push
|
||||
</option>
|
||||
<option value="steam">
|
||||
Steam Game Server
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -46,11 +49,11 @@
|
|||
|
||||
<!-- Push URL -->
|
||||
<div v-if="monitor.type === 'push' " class="my-3">
|
||||
<label for="push-url" class="form-label">{{ $t("Push URL") }}</label>
|
||||
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
||||
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
|
||||
<div class="form-text">
|
||||
You should call this url every {{ monitor.interval }} seconds.<br />
|
||||
Optional parameters: msg, ping
|
||||
{{ $t("needPushEvery", [monitor.interval]) }}<br />
|
||||
{{ $t("pushOptionalParams", ["msg, ping"]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -63,18 +66,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TCP Port / Ping / DNS only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3">
|
||||
<!-- Hostname -->
|
||||
<!-- TCP Port / Ping / DNS / Steam only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam'" class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
|
||||
</div>
|
||||
|
||||
<!-- For TCP Port Type -->
|
||||
<div v-if="monitor.type === 'port' " class="my-3">
|
||||
<!-- Port -->
|
||||
<!-- For TCP Port / Steam Type -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam'" class="my-3">
|
||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<!-- DNS Resolver Server -->
|
||||
<!-- For DNS Type -->
|
||||
<template v-if="monitor.type === 'dns'">
|
||||
<div class="my-3">
|
||||
|
@ -140,7 +146,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type !== 'push'" class="my-3 form-check">
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
{{ $t("Upside Down Mode") }}
|
||||
|
@ -195,6 +201,7 @@
|
|||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
|
@ -214,6 +221,51 @@
|
|||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
|
||||
<!-- HTTP Options -->
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
||||
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
|
||||
|
||||
<!-- Method -->
|
||||
<div class="my-3">
|
||||
<label for="method" class="form-label">{{ $t("Method") }}</label>
|
||||
<select id="method" v-model="monitor.method" class="form-select">
|
||||
<option value="GET">
|
||||
GET
|
||||
</option>
|
||||
<option value="POST">
|
||||
POST
|
||||
</option>
|
||||
<option value="PUT">
|
||||
PUT
|
||||
</option>
|
||||
<option value="PATCH">
|
||||
PATCH
|
||||
</option>
|
||||
<option value="DELETE">
|
||||
DELETE
|
||||
</option>
|
||||
<option value="HEAD">
|
||||
HEAD
|
||||
</option>
|
||||
<option value="OPTIONS">
|
||||
OPTIONS
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="my-3">
|
||||
<label for="body" class="form-label">{{ $t("Body") }}</label>
|
||||
<textarea id="body" v-model="monitor.body" class="form-control" :placeholder="bodyPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Headers -->
|
||||
<div class="my-3">
|
||||
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
|
||||
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -285,6 +337,14 @@ export default {
|
|||
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
|
||||
},
|
||||
|
||||
bodyPlaceholder() {
|
||||
return "{\n\t\"id\": 124357,\n\t\"username\": \"admin\",\n\t\"password\": \"myAdminPassword\"\n}";
|
||||
},
|
||||
|
||||
headersPlaceholder() {
|
||||
return "{\n\t\"Authorization\": \"Bearer abc123\",\n\t\"Content-Type\": \"application/json\"\n}";
|
||||
}
|
||||
|
||||
},
|
||||
|
@ -295,7 +355,7 @@ export default {
|
|||
},
|
||||
|
||||
"monitor.interval"(value, oldValue) {
|
||||
// Link interval and retryInerval if they are the same value.
|
||||
// Link interval and retryInterval if they are the same value.
|
||||
if (this.monitor.retryInterval === oldValue) {
|
||||
this.monitor.retryInterval = value;
|
||||
}
|
||||
|
@ -349,6 +409,7 @@ export default {
|
|||
type: "http",
|
||||
name: "",
|
||||
url: "https://",
|
||||
method: "GET",
|
||||
interval: 60,
|
||||
retryInterval: this.interval,
|
||||
maxretries: 0,
|
||||
|
@ -383,9 +444,43 @@ export default {
|
|||
|
||||
},
|
||||
|
||||
isInputValid() {
|
||||
if (this.monitor.body) {
|
||||
try {
|
||||
JSON.parse(this.monitor.body);
|
||||
} catch (err) {
|
||||
toast.error(this.$t("BodyInvalidFormat") + err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.monitor.headers) {
|
||||
try {
|
||||
JSON.parse(this.monitor.headers);
|
||||
} catch (err) {
|
||||
toast.error(this.$t("HeadersInvalidFormat") + err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (!this.isInputValid()) {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Beautiful the JSON format
|
||||
if (this.monitor.body) {
|
||||
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
|
||||
}
|
||||
|
||||
if (this.monitor.headers) {
|
||||
this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4);
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.add(this.monitor, async (res) => {
|
||||
|
||||
|
@ -422,8 +517,12 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
<!-- Primary Base URL -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">Primary Base URL</label>
|
||||
<label class="form-label" for="primaryBaseURL">{{ $t("Primary Base URL") }}</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+">
|
||||
|
@ -119,6 +119,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Steam API Key -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="steamAPIKey">{{ $t("Steam API Key") }}</label>
|
||||
<input id="steamAPIKey" v-model="settings.steamAPIKey" class="form-control" name="steamAPIKey">
|
||||
<div class="form-text">
|
||||
{{ $t("steamApiKeyDescription") }}<a href="https://steamcommunity.com/dev" target="_blank">https://steamcommunity.com/dev</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitor History -->
|
||||
<div class="mb-4">
|
||||
<h4 class="mt-4">{{ $t("Monitor History") }}</h4>
|
||||
<div class="mt-2">
|
||||
<label for="keepDataPeriodDays" class="form-label">{{ $t("clearDataOlderThan", [ settings.keepDataPeriodDays ]) }}</label>
|
||||
<input id="keepDataPeriodDays" v-model="settings.keepDataPeriodDays" type="number" class="form-control" required min="1" step="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Save") }}
|
||||
|
@ -334,9 +353,9 @@
|
|||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'id-ID' ">
|
||||
<p> Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>? </p>
|
||||
<p> Ini untuk <strong>mereka yang memiliki otentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare. </p>
|
||||
<p> Gunakan dengan hati-hati. </p>
|
||||
<p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
|
||||
<p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
|
||||
<p>Gunakan dengan hati-hati.</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
||||
|
@ -477,6 +496,10 @@ export default {
|
|||
this.settings.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
if (this.settings.keepDataPeriodDays === undefined) {
|
||||
this.settings.keepDataPeriodDays = 180;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
const { genSecret } = require("../src/util");
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
});
|
||||
const { genSecret, sleep } = require("../src/util");
|
||||
|
||||
describe("Test genSecret", () => {
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
});
|
||||
|
||||
it("should be correct length", () => {
|
||||
let secret = genSecret(-1);
|
||||
expect(secret).toEqual("");
|
||||
|
@ -34,4 +34,11 @@ describe("Test genSecret", () => {
|
|||
expect(secret).toContain("A");
|
||||
expect(secret).toContain("9");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Test reset-password", () => {
|
||||
it("should able to run", async () => {
|
||||
await require("../extra/reset-password").main();
|
||||
}, 120000);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"newLine": "LF",
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
|
|
Loading…
Reference in a new issue