mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-24 07:14:04 +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
|
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!
|
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.
|
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* 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).
|
* 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)
|
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
|
||||||
* Simple Status Page
|
* Simple Status Page
|
||||||
* Ping Chart
|
* 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
|
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
|
### 💪🏻 Without Docker
|
||||||
|
|
||||||
|
@ -58,11 +58,11 @@ npm run setup
|
||||||
node server/server.js
|
node server/server.js
|
||||||
|
|
||||||
# (Recommended) Option 2. Run in background using PM2
|
# (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
|
pm2 start server/server.js --name uptime-kuma
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after started.
|
Browse to http://localhost:3001 after starting.
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-k
|
||||||
### Subreddit
|
### Subreddit
|
||||||
|
|
||||||
My Reddit account: louislamlam
|
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/
|
https://www.reddit.com/r/UptimeKuma/
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
|
@ -9,8 +9,8 @@ currently being supported with security updates.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 1.7.X | :white_check_mark: |
|
| 1.8.X | :white_check_mark: |
|
||||||
| < 1.7 | ❌ |
|
| <= 1.7.X | ❌ |
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
### 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
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
const main = async () => {
|
||||||
Database.init(args);
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await R.findOne("user");
|
// 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) {
|
||||||
if (! user) {
|
const user = await R.findOne("user");
|
||||||
throw new Error("user not found, have you installed?");
|
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.");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error("Error: " + e.message);
|
console.error("Error: " + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
rl.close();
|
||||||
|
|
||||||
console.log("Finished. You should restart the Uptime Kuma server.")
|
console.log("Finished.");
|
||||||
})();
|
};
|
||||||
|
|
||||||
function question(question) {
|
function question(question) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
rl.question(question, (answer) => {
|
rl.question(question, (answer) => {
|
||||||
resolve(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";
|
const baseLangCode = process.argv[2] || "en";
|
||||||
console.log("Base Lang: " + baseLangCode);
|
console.log("Base Lang: " + baseLangCode);
|
||||||
fs.rmdirSync("./languages", { recursive: true });
|
if (fs.existsSync("./languages")) {
|
||||||
|
fs.rmdirSync("./languages", { recursive: true });
|
||||||
|
}
|
||||||
copyRecursiveSync("../../src/languages", "./languages");
|
copyRecursiveSync("../../src/languages", "./languages");
|
||||||
|
|
||||||
const en = (await import("./languages/en.js")).default;
|
const en = (await import("./languages/en.js")).default;
|
||||||
|
@ -39,7 +41,7 @@ console.log("Files:", files);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.endsWith(".js")) {
|
if (!file.endsWith(".js")) {
|
||||||
console.log("Skipping " + file)
|
console.log("Skipping " + file);
|
||||||
continue;
|
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",
|
"@popperjs/core": "~2.10.2",
|
||||||
"args-parser": "~1.3.0",
|
"args-parser": "~1.3.0",
|
||||||
"axios": "~0.21.4",
|
"axios": "~0.21.4",
|
||||||
"babel-plugin-rewire": "~1.2.0",
|
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"bootstrap": "~5.1.1",
|
"bootstrap": "~5.1.1",
|
||||||
|
"chardet": "^1.3.0",
|
||||||
|
"bree": "~6.3.1",
|
||||||
"chart.js": "~3.5.1",
|
"chart.js": "~3.5.1",
|
||||||
"chartjs-adapter-dayjs": "~1.0.0",
|
"chartjs-adapter-dayjs": "~1.0.0",
|
||||||
"command-exists": "~1.2.9",
|
"command-exists": "~1.2.9",
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
"express-basic-auth": "~1.2.0",
|
"express-basic-auth": "~1.2.0",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"http-graceful-shutdown": "~3.1.4",
|
"http-graceful-shutdown": "~3.1.4",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"nodemailer": "~6.6.5",
|
"nodemailer": "~6.6.5",
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
|
@ -108,6 +110,7 @@
|
||||||
"@vitejs/plugin-legacy": "~1.6.1",
|
"@vitejs/plugin-legacy": "~1.6.1",
|
||||||
"@vitejs/plugin-vue": "~1.9.2",
|
"@vitejs/plugin-vue": "~1.9.2",
|
||||||
"@vue/compiler-sfc": "~3.2.19",
|
"@vue/compiler-sfc": "~3.2.19",
|
||||||
|
"babel-plugin-rewire": "~1.2.0",
|
||||||
"core-js": "~3.18.1",
|
"core-js": "~3.18.1",
|
||||||
"cross-env": "~7.0.3",
|
"cross-env": "~7.0.3",
|
||||||
"dns2": "~2.0.1",
|
"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-incident-table.sql": true,
|
||||||
"patch-group-table.sql": true,
|
"patch-group-table.sql": true,
|
||||||
"patch-monitor-push_token.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
|
* @deprecated Use patchList for any new feature
|
||||||
*/
|
*/
|
||||||
static latestVersion = 10;
|
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 axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
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 { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification");
|
const { Notification } = require("../notification");
|
||||||
const { demoMode } = require("../server");
|
const { demoMode } = require("../config");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
|
||||||
|
@ -55,6 +55,9 @@ class Monitor extends BeanModel {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
|
method: this.method,
|
||||||
|
body: this.body,
|
||||||
|
headers: this.headers,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
|
@ -138,11 +141,15 @@ class Monitor extends BeanModel {
|
||||||
// Do not do any queries/high loading things before the "bean.ping"
|
// Do not do any queries/high loading things before the "bean.ping"
|
||||||
let startTime = dayjs().valueOf();
|
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,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"User-Agent": "Uptime-Kuma/" + version,
|
"User-Agent": "Uptime-Kuma/" + version,
|
||||||
|
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||||
},
|
},
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: new https.Agent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
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) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
let res = await axios.request(options);
|
||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
|
@ -172,6 +180,10 @@ class Monitor extends BeanModel {
|
||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
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") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
|
@ -260,6 +272,46 @@ class Monitor extends BeanModel {
|
||||||
return;
|
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 {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
bean.msg = "Unknown Monitor Type";
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
|
@ -292,54 +344,13 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
let beatInterval = this.interval;
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Mark as important if status changed, ignore pending pings,
|
// Mark as important if status changed, ignore pending pings,
|
||||||
// Don't notify if disrupted changes to up
|
// Don't notify if disrupted changes to up
|
||||||
if (isImportant) {
|
if (isImportant) {
|
||||||
bean.important = true;
|
bean.important = true;
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
}
|
}
|
||||||
|
@ -546,6 +557,53 @@ class Monitor extends BeanModel {
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
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;
|
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 time = heartbeatJSON["time"];
|
||||||
|
const textMsg = "Uptime Kuma Alert";
|
||||||
let data = {
|
let data = {
|
||||||
"text": "Uptime Kuma Alert",
|
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||||
"channel": notification.slackchannel,
|
"channel": notification.slackchannel,
|
||||||
"username": notification.slackusername,
|
"username": notification.slackusername,
|
||||||
"icon_emoji": notification.slackiconemo,
|
"icon_emoji": notification.slackiconemo,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
class SMTP extends NotificationProvider {
|
class SMTP extends NotificationProvider {
|
||||||
|
|
||||||
|
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
|
||||||
pass: notification.smtpPassword,
|
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);
|
let transporter = nodemailer.createTransport(config);
|
||||||
|
|
||||||
|
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
|
||||||
cc: notification.smtpCC,
|
cc: notification.smtpCC,
|
||||||
bcc: notification.smtpBCC,
|
bcc: notification.smtpBCC,
|
||||||
to: notification.smtpTo,
|
to: notification.smtpTo,
|
||||||
subject: msg,
|
subject: subject,
|
||||||
text: bodyTextContent,
|
text: bodyTextContent,
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||||
|
|
|
@ -19,6 +19,8 @@ const Teams = require("./notification-providers/teams");
|
||||||
const Telegram = require("./notification-providers/telegram");
|
const Telegram = require("./notification-providers/telegram");
|
||||||
const Webhook = require("./notification-providers/webhook");
|
const Webhook = require("./notification-providers/webhook");
|
||||||
const Feishu = require("./notification-providers/feishu");
|
const Feishu = require("./notification-providers/feishu");
|
||||||
|
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||||
|
const DingDing = require("./notification-providers/dingding");
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
|
@ -31,6 +33,8 @@ class Notification {
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
new Apprise(),
|
new Apprise(),
|
||||||
|
new AliyunSms(),
|
||||||
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Teams(),
|
new Teams(),
|
||||||
new Gotify(),
|
new Gotify(),
|
||||||
|
|
|
@ -4,10 +4,7 @@ const net = require("net");
|
||||||
const spawn = require("child_process").spawn;
|
const spawn = require("child_process").spawn;
|
||||||
const events = require("events");
|
const events = require("events");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const WIN = /^win/.test(process.platform);
|
const util = require("./util-server");
|
||||||
const LIN = /^linux/.test(process.platform);
|
|
||||||
const MAC = /^darwin/.test(process.platform);
|
|
||||||
const FBSD = /^freebsd/.test(process.platform);
|
|
||||||
|
|
||||||
module.exports = Ping;
|
module.exports = Ping;
|
||||||
|
|
||||||
|
@ -23,12 +20,12 @@ function Ping(host, options) {
|
||||||
|
|
||||||
const timeout = 10;
|
const timeout = 10;
|
||||||
|
|
||||||
if (WIN) {
|
if (util.WIN) {
|
||||||
this._bin = "c:/windows/system32/ping.exe";
|
this._bin = "c:/windows/system32/ping.exe";
|
||||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
||||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||||
|
|
||||||
} else if (LIN) {
|
} else if (util.LIN) {
|
||||||
this._bin = "/bin/ping";
|
this._bin = "/bin/ping";
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
||||||
|
@ -40,7 +37,7 @@ function Ping(host, options) {
|
||||||
this._args = (options.args) ? options.args : defaultArgs;
|
this._args = (options.args) ? options.args : defaultArgs;
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
|
|
||||||
} else if (MAC) {
|
} else if (util.MAC) {
|
||||||
|
|
||||||
if (net.isIPv6(host) || options.ipv6) {
|
if (net.isIPv6(host) || options.ipv6) {
|
||||||
this._bin = "/sbin/ping6";
|
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._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
|
|
||||||
} else if (FBSD) {
|
} else if (util.FBSD) {
|
||||||
this._bin = "/sbin/ping";
|
this._bin = "/sbin/ping";
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
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
|
this._ping.stdout.on("data", function (data) { // log stdout
|
||||||
|
if (util.WIN) {
|
||||||
|
data = convertOutput(data);
|
||||||
|
}
|
||||||
this._stdout = (this._stdout || "") + data;
|
this._stdout = (this._stdout || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stderr.on("data", function (data) { // log stderr
|
this._ping.stderr.on("data", function (data) { // log stderr
|
||||||
|
if (util.WIN) {
|
||||||
|
data = convertOutput(data);
|
||||||
|
}
|
||||||
this._stderr = (this._stderr || "") + data;
|
this._stderr = (this._stderr || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
|
||||||
Ping.prototype.stop = function () {
|
Ping.prototype.stop = function () {
|
||||||
clearInterval(this._i);
|
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 apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP } = require("../../src/util");
|
const { UP, flipStatus, debug } = require("../../src/util");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
|
@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
|
||||||
|
|
||||||
router.get("/api/push/:pushToken", async (request, response) => {
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
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 ", [
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
pushToken
|
pushToken
|
||||||
|
@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
throw new Error("Monitor not found or not active.");
|
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");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = monitor.id;
|
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
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.msg = msg;
|
||||||
bean.ping = ping;
|
bean.ping = ping;
|
||||||
|
bean.duration = duration;
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
response.json({
|
response.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (bean.important) {
|
||||||
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.json({
|
response.json({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
console.log("Welcome to Uptime Kuma");
|
console.log("Welcome to Uptime Kuma");
|
||||||
const args = require("args-parser")(process.argv);
|
const args = require("args-parser")(process.argv);
|
||||||
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
debug(args);
|
debug(args);
|
||||||
|
|
||||||
|
@ -8,10 +9,6 @@ if (! process.env.NODE_ENV) {
|
||||||
process.env.NODE_ENV = "production";
|
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("Node Env: " + process.env.NODE_ENV);
|
||||||
|
|
||||||
console.log("Importing Node libraries");
|
console.log("Importing Node libraries");
|
||||||
|
@ -43,7 +40,7 @@ console.log("Importing this project modules");
|
||||||
debug("Importing Monitor");
|
debug("Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
debug("Importing Settings");
|
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");
|
debug("Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
|
@ -52,6 +49,9 @@ Notification.init();
|
||||||
debug("Importing Database");
|
debug("Importing Database");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
|
||||||
|
debug("Importing Background Jobs");
|
||||||
|
const { initBackgroundJobs } = require("./jobs");
|
||||||
|
|
||||||
const { basicAuth } = require("./auth");
|
const { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
const passwordHash = require("./password-hash");
|
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.
|
// 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 (::)
|
// Dual-stack support for (::)
|
||||||
const hostname = process.env.HOST || args.host;
|
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
|
||||||
|
// 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
|
// SSL
|
||||||
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
|
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||||
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || 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
|
* 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;
|
const testMode = !!args["test"] || false;
|
||||||
|
|
||||||
if (demoMode) {
|
if (config.demoMode) {
|
||||||
console.log("==== Demo Mode ====");
|
console.log("==== Demo Mode ====");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +281,7 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.token) {
|
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) {
|
if (verify && verify.delta == 0) {
|
||||||
callback({
|
callback({
|
||||||
|
@ -383,7 +399,7 @@ exports.entryPage = "dashboard";
|
||||||
socket.userID,
|
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) {
|
if (verify && verify.delta == 0) {
|
||||||
callback({
|
callback({
|
||||||
|
@ -509,6 +525,9 @@ exports.entryPage = "dashboard";
|
||||||
bean.name = monitor.name;
|
bean.name = monitor.name;
|
||||||
bean.type = monitor.type;
|
bean.type = monitor.type;
|
||||||
bean.url = monitor.url;
|
bean.url = monitor.url;
|
||||||
|
bean.method = monitor.method;
|
||||||
|
bean.body = monitor.body;
|
||||||
|
bean.headers = monitor.headers;
|
||||||
bean.interval = monitor.interval;
|
bean.interval = monitor.interval;
|
||||||
bean.retryInterval = monitor.retryInterval;
|
bean.retryInterval = monitor.retryInterval;
|
||||||
bean.hostname = monitor.hostname;
|
bean.hostname = monitor.hostname;
|
||||||
|
@ -1034,6 +1053,9 @@ exports.entryPage = "dashboard";
|
||||||
name: monitorListData[i].name,
|
name: monitorListData[i].name,
|
||||||
type: monitorListData[i].type,
|
type: monitorListData[i].type,
|
||||||
url: monitorListData[i].url,
|
url: monitorListData[i].url,
|
||||||
|
method: monitorListData[i].method || "GET",
|
||||||
|
body: monitorListData[i].body,
|
||||||
|
headers: monitorListData[i].headers,
|
||||||
interval: monitorListData[i].interval,
|
interval: monitorListData[i].interval,
|
||||||
retryInterval: retryInterval,
|
retryInterval: retryInterval,
|
||||||
hostname: monitorListData[i].hostname,
|
hostname: monitorListData[i].hostname,
|
||||||
|
@ -1239,6 +1261,8 @@ exports.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initBackgroundJobs(args);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
|
|
|
@ -6,6 +6,14 @@ const passwordHash = require("./password-hash");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { Resolver } = require("dns");
|
const { Resolver } = require("dns");
|
||||||
const child_process = require("child_process");
|
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
|
* 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` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
]);
|
]);
|
||||||
|
@ -124,6 +132,7 @@ exports.setSetting = async function (key, value) {
|
||||||
bean = R.dispense("setting");
|
bean = R.dispense("setting");
|
||||||
bean.key = key;
|
bean.key = key;
|
||||||
}
|
}
|
||||||
|
bean.type = type;
|
||||||
bean.value = JSON.stringify(value);
|
bean.value = JSON.stringify(value);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
};
|
};
|
||||||
|
@ -312,3 +321,14 @@ exports.startUnitTest = async () => {
|
||||||
process.exit(code);
|
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;
|
font-size: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
border-radius: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__tag {
|
.multiselect__tag {
|
||||||
border-radius: 50rem;
|
border-radius: $border-radius;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 6px 26px 6px 10px;
|
padding: 6px 26px 6px 10px;
|
||||||
background: $primary !important;
|
background: $primary !important;
|
||||||
|
|
|
@ -186,7 +186,7 @@ export default {
|
||||||
.beat {
|
.beat {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
border-radius: 50rem;
|
border-radius: $border-radius;
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
background-color: aliceblue;
|
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">
|
<div class="form-text">
|
||||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||||
</div>
|
</div>
|
||||||
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
|
||||||
<a
|
<a
|
||||||
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3">
|
<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">
|
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<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">
|
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<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>
|
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-text">
|
<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;">
|
<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.
|
{{ $t("matrixDesc1") }}
|
||||||
</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>.
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -30,5 +30,5 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Do you use the legacy version of Octopush (2011-2020) or the new version?
|
{{ $t("octopushLegacyHint") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
|
@ -13,9 +13,10 @@
|
||||||
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
<option value="2">{{ $t("promosmsTypeFull") }}</option>
|
||||||
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
<option value="3">{{ $t("promosmsTypeSpeed") }}</option>
|
||||||
</select>
|
</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>
|
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
|
||||||
</i18n-t>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
|
<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>
|
<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">
|
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -57,6 +57,18 @@
|
||||||
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
|
<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">
|
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
|
<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>
|
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
<div class="form-text">
|
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
|
||||||
{{ $t("You can get a token from") }} <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
|
||||||
</div>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="form-text">
|
<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">
|
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||||
<template #multipart>"multipart/form-data"</template>
|
<template #multipart>"multipart/form-data"</template>
|
||||||
<template #decodeFunction>
|
<template #decodeFunction>
|
||||||
|
|
|
@ -18,6 +18,8 @@ import Pushbullet from "./Pushbullet.vue";
|
||||||
import Line from "./Line.vue";
|
import Line from "./Line.vue";
|
||||||
import Mattermost from "./Mattermost.vue";
|
import Mattermost from "./Mattermost.vue";
|
||||||
import Matrix from "./Matrix.vue";
|
import Matrix from "./Matrix.vue";
|
||||||
|
import AliyunSMS from "./AliyunSms.vue";
|
||||||
|
import DingDing from "./DingDing.vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage all notification form.
|
* Manage all notification form.
|
||||||
|
@ -40,11 +42,13 @@ const NotificationFormList = {
|
||||||
"promosms": PromoSMS,
|
"promosms": PromoSMS,
|
||||||
"lunasea": LunaSea,
|
"lunasea": LunaSea,
|
||||||
"Feishu": Feishu,
|
"Feishu": Feishu,
|
||||||
|
"AliyunSMS": AliyunSMS,
|
||||||
"apprise": Apprise,
|
"apprise": Apprise,
|
||||||
"pushbullet": Pushbullet,
|
"pushbullet": Pushbullet,
|
||||||
"line": Line,
|
"line": Line,
|
||||||
"mattermost": Mattermost,
|
"mattermost": Mattermost,
|
||||||
"matrix": Matrix,
|
"matrix": Matrix,
|
||||||
|
"DingDing": DingDing
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationFormList
|
export default NotificationFormList
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
||||||
Appearance: "Appearance",
|
Appearance: "Appearance",
|
||||||
Theme: "Theme",
|
Theme: "Theme",
|
||||||
General: "General",
|
General: "General",
|
||||||
|
"Primary Base URL": "Primary Base URL",
|
||||||
Version: "Version",
|
Version: "Version",
|
||||||
"Check Update On GitHub": "Check Update On GitHub",
|
"Check Update On GitHub": "Check Update On GitHub",
|
||||||
List: "List",
|
List: "List",
|
||||||
|
@ -75,6 +76,9 @@ export default {
|
||||||
"Upside Down Mode": "Upside Down Mode",
|
"Upside Down Mode": "Upside Down Mode",
|
||||||
"Max. Redirects": "Max. Redirects",
|
"Max. Redirects": "Max. Redirects",
|
||||||
"Accepted Status Codes": "Accepted Status Codes",
|
"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",
|
Save: "Save",
|
||||||
Notifications: "Notifications",
|
Notifications: "Notifications",
|
||||||
"Not available, please setup.": "Not available, please setup.",
|
"Not available, please setup.": "Not available, please setup.",
|
||||||
|
@ -185,7 +189,7 @@ export default {
|
||||||
"Required": "Required",
|
"Required": "Required",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"Bot Token": "Bot Token",
|
"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",
|
"Chat ID": "Chat ID",
|
||||||
supportTelegramChatID: "Support Direct Chat / Group / Channel's 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:",
|
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)",
|
secureOptionTLS: "TLS (465)",
|
||||||
"Ignore TLS Error": "Ignore TLS Error",
|
"Ignore TLS Error": "Ignore TLS Error",
|
||||||
"From Email": "From Email",
|
"From Email": "From Email",
|
||||||
|
emailCustomSubject: "Custom Subject",
|
||||||
"To Email": "To Email",
|
"To Email": "To Email",
|
||||||
smtpCC: "CC",
|
smtpCC: "CC",
|
||||||
smtpBCC: "BCC",
|
smtpBCC: "BCC",
|
||||||
|
@ -251,6 +256,8 @@ export default {
|
||||||
"SMS Type": "SMS Type",
|
"SMS Type": "SMS Type",
|
||||||
octopushTypePremium: "Premium (Fast - recommended for alerting)",
|
octopushTypePremium: "Premium (Fast - recommended for alerting)",
|
||||||
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)",
|
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}.",
|
"Check octopush prices": "Check octopush prices {0}.",
|
||||||
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
|
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
|
||||||
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
|
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)",
|
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",
|
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||||
"Feishu WebHookUrl": "Feishu WebHookUrl",
|
"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
|
// 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.",
|
passwordNotMatchMsg: "La contraseña repetida no coincide.",
|
||||||
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
|
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",
|
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?",
|
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
|
||||||
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
|
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.",
|
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",
|
Down: "Caído",
|
||||||
Pending: "Pendiente",
|
Pending: "Pendiente",
|
||||||
Unknown: "Desconocido",
|
Unknown: "Desconocido",
|
||||||
Pause: "Pausa",
|
Pause: "Pausar",
|
||||||
Name: "Nombre",
|
Name: "Nombre",
|
||||||
Status: "Estado",
|
Status: "Estado",
|
||||||
DateTime: "Fecha y Hora",
|
DateTime: "Fecha y Hora",
|
||||||
|
@ -198,4 +198,9 @@ export default {
|
||||||
pushbullet: "Pushbullet",
|
pushbullet: "Pushbullet",
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
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",
|
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.",
|
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.",
|
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 ?",
|
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.",
|
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.",
|
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",
|
Delete: "Supprimer",
|
||||||
Current: "Actuellement",
|
Current: "Actuellement",
|
||||||
Uptime: "Uptime",
|
Uptime: "Uptime",
|
||||||
"Cert Exp.": "Certificat expiré",
|
"Cert Exp.": "Expiration SSL",
|
||||||
days: "jours",
|
days: "jours",
|
||||||
day: "jour",
|
day: "jour",
|
||||||
"-day": "-jours",
|
"-day": "-jours",
|
||||||
|
@ -185,7 +185,7 @@ export default {
|
||||||
"Required": "Requis",
|
"Required": "Requis",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"Bot Token": "Bot Token",
|
"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",
|
"Chat ID": "Chat ID",
|
||||||
supportTelegramChatID: "Supporte les messages privés / en groupe / l'ID du salon",
|
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 :",
|
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 {
|
export default {
|
||||||
languageName: "Indonesia",
|
languageName: "Bahasa Indonesia (Indonesian)",
|
||||||
checkEverySecond: "Cek Setiap {0} detik.",
|
checkEverySecond: "Cek Setiap {0} detik.",
|
||||||
retryCheckEverySecond: "Coba lagi setiap {0} detik.",
|
retryCheckEverySecond: "Coba lagi setiap {0} detik.",
|
||||||
retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim",
|
retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim",
|
||||||
ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS",
|
ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS",
|
||||||
upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
|
upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
|
||||||
maxRedirectDescription: "Jumlah maksimum pengalihan untuk diikuti. Setel ke 0 untuk menonaktifkan pengalihan.",
|
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.",
|
passwordNotMatchMsg: "Sandi kedua tidak cocok.",
|
||||||
notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.",
|
notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.",
|
||||||
keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
|
keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
|
||||||
pauseDashboardHome: "Jeda",
|
pauseDashboardHome: "Jeda",
|
||||||
deleteMonitorMsg: "Apakah anda mau menghapus monitor ini?",
|
deleteMonitorMsg: "Apakah Anda mau menghapus monitor ini?",
|
||||||
deleteNotificationMsg: "Apakah anda mau menghapus notifikasi ini untuk semua monitor?",
|
deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi ini untuk semua monitor?",
|
||||||
resoverserverDescription: "Cloudflare adalah server default, Anda dapat mengubah server resolver kapan saja.",
|
resoverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
|
||||||
rrtypeDescription: "Pilih RR-Type yang mau anda monitor",
|
rrtypeDescription: "Pilih RR-Type yang mau Anda monitor",
|
||||||
pauseMonitorMsg: "Apakah anda yakin mau menjeda?",
|
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.",
|
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?",
|
clearEventsMsg: "Apakah Anda yakin mau menghapus semua event di monitor ini?",
|
||||||
clearHeartbeatsMsg: "Apakah anda yakin mau menghapus semua heartbeats di monitor ini?",
|
clearHeartbeatsMsg: "Apakah Anda yakin mau menghapus semua heartbeats di monitor ini?",
|
||||||
confirmClearStatisticsMsg: "Apakah anda yakin mau menghapus semua statistik?",
|
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.",
|
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.",
|
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",
|
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?",
|
confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
|
||||||
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
|
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
|
||||||
Settings: "Pengaturan",
|
Settings: "Pengaturan",
|
||||||
Dashboard: "Dashboard",
|
Dashboard: "Dasbor",
|
||||||
"New Update": "Update Baru",
|
"New Update": "Pembaruan Baru",
|
||||||
Language: "Bahasa",
|
Language: "Bahasa",
|
||||||
Appearance: "Tampilan",
|
Appearance: "Tampilan",
|
||||||
Theme: "Tema",
|
Theme: "Tema",
|
||||||
General: "General",
|
General: "Umum",
|
||||||
Version: "Versi",
|
Version: "Versi",
|
||||||
"Check Update On GitHub": "Cek Update di GitHub",
|
"Check Update On GitHub": "Cek Pembaruan di GitHub",
|
||||||
List: "List",
|
List: "Daftar",
|
||||||
Add: "Tambah",
|
Add: "Tambah",
|
||||||
"Add New Monitor": "Tambah Monitor Baru",
|
"Add New Monitor": "Tambah Monitor Baru",
|
||||||
"Quick Stats": "Statistik Cepat",
|
"Quick Stats": "Statistik",
|
||||||
Up: "Aktif",
|
Up: "Aktif",
|
||||||
Down: "Tidak Aktif",
|
Down: "Tidak Aktif",
|
||||||
Pending: "Tertunda",
|
Pending: "Tertunda",
|
||||||
|
@ -48,9 +48,9 @@ export default {
|
||||||
Status: "Status",
|
Status: "Status",
|
||||||
DateTime: "Tanggal Waktu",
|
DateTime: "Tanggal Waktu",
|
||||||
Message: "Pesan",
|
Message: "Pesan",
|
||||||
"No important events": "Tidak ada Event penting",
|
"No important events": "Tidak ada peristiwa penting",
|
||||||
Resume: "Melanjutkan",
|
Resume: "Lanjut",
|
||||||
Edit: "Rubah",
|
Edit: "Ubah",
|
||||||
Delete: "Hapus",
|
Delete: "Hapus",
|
||||||
Current: "Saat ini",
|
Current: "Saat ini",
|
||||||
Uptime: "Waktu aktif",
|
Uptime: "Waktu aktif",
|
||||||
|
@ -60,20 +60,20 @@ export default {
|
||||||
"-day": "-hari",
|
"-day": "-hari",
|
||||||
hour: "Jam",
|
hour: "Jam",
|
||||||
"-hour": "-Jam",
|
"-hour": "-Jam",
|
||||||
Response: "Respon",
|
Response: "Tanggapan",
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Tipe Monitor",
|
"Monitor Type": "Tipe Monitor",
|
||||||
Keyword: "Keyword",
|
Keyword: "Keyword",
|
||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Nama yang Ramah",
|
||||||
URL: "URL",
|
URL: "URL",
|
||||||
Hostname: "Hostname",
|
Hostname: "Hostname",
|
||||||
Port: "Port",
|
Port: "Port",
|
||||||
"Heartbeat Interval": "Interval Heartbeat ",
|
"Heartbeat Interval": "Jarak Waktu Heartbeat ",
|
||||||
Retries: "Retries",
|
Retries: "Coba lagi",
|
||||||
"Heartbeat Retry Interval": "Interval Heartbeat Mencoba kembali ",
|
"Heartbeat Retry Interval": "Jarak Waktu Heartbeat Mencoba kembali ",
|
||||||
Advanced: "Advanced",
|
Advanced: "Tingkat Lanjut",
|
||||||
"Upside Down Mode": "Mode Terbalik",
|
"Upside Down Mode": "Mode Terbalik",
|
||||||
"Max. Redirects": "Maksimal Redirect/Pengalihan",
|
"Max. Redirects": "Maksimal Pengalihan",
|
||||||
"Accepted Status Codes": "Kode Status yang Diterima",
|
"Accepted Status Codes": "Kode Status yang Diterima",
|
||||||
Save: "Simpan",
|
Save: "Simpan",
|
||||||
Notifications: "Notifikasi",
|
Notifications: "Notifikasi",
|
||||||
|
@ -81,25 +81,25 @@ export default {
|
||||||
"Setup Notification": "Setel Notifikasi",
|
"Setup Notification": "Setel Notifikasi",
|
||||||
Light: "Terang",
|
Light: "Terang",
|
||||||
Dark: "Gelap",
|
Dark: "Gelap",
|
||||||
Auto: "Automatis",
|
Auto: "Otomatis",
|
||||||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
|
"Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
|
||||||
Normal: "Normal",
|
Normal: "Normal",
|
||||||
Bottom: "Bawah",
|
Bottom: "Bawah",
|
||||||
None: "Tidak ada",
|
None: "Tidak ada",
|
||||||
Timezone: "Zona Waktu",
|
Timezone: "Zona Waktu",
|
||||||
"Search Engine Visibility": "Visibilitas Mesin Pencari",
|
"Search Engine Visibility": "Visibilitas Mesin Pencari",
|
||||||
"Allow indexing": "Mengizinkan untuk diindex",
|
"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",
|
"Change Password": "Ganti Sandi",
|
||||||
"Current Password": "Sandi Lama",
|
"Current Password": "Sandi Lama",
|
||||||
"New Password": "Sandi Baru",
|
"New Password": "Sandi Baru",
|
||||||
"Repeat New Password": "Ulangi Sandi Baru",
|
"Repeat New Password": "Ulangi Sandi Baru",
|
||||||
"Update Password": "Perbarui Kata Sandi",
|
"Update Password": "Perbarui Kata Sandi",
|
||||||
"Disable Auth": "Nonaktifkan auth",
|
"Disable Auth": "Nonaktifkan Autentikasi",
|
||||||
"Enable Auth": "Aktifkan Auth",
|
"Enable Auth": "Aktifkan Autentikasi",
|
||||||
Logout: "Keluar",
|
Logout: "Keluar",
|
||||||
Leave: "Pergi",
|
Leave: "Pergi",
|
||||||
"I understand, please disable": "Saya mengerti, silahkan dinonaktifkan",
|
"I understand, please disable": "Saya mengerti, silakan dinonaktifkan",
|
||||||
Confirm: "Konfirmasi",
|
Confirm: "Konfirmasi",
|
||||||
Yes: "Ya",
|
Yes: "Ya",
|
||||||
No: "Tidak",
|
No: "Tidak",
|
||||||
|
@ -107,35 +107,35 @@ export default {
|
||||||
Password: "Sandi",
|
Password: "Sandi",
|
||||||
"Remember me": "Ingat saya",
|
"Remember me": "Ingat saya",
|
||||||
Login: "Masuk",
|
Login: "Masuk",
|
||||||
"No Monitors, please": "Tidak ada monitor, silahkan",
|
"No Monitors, please": "Tidak ada monitor, silakan",
|
||||||
"add one": "tambah baru",
|
"add one": "tambahkan satu",
|
||||||
"Notification Type": "Tipe Notifikasi",
|
"Notification Type": "Tipe Notifikasi",
|
||||||
Email: "Email",
|
Email: "Surel",
|
||||||
Test: "Test",
|
Test: "Tes",
|
||||||
"Certificate Info": "Info Sertifikasi ",
|
"Certificate Info": "Info Sertifikasi",
|
||||||
"Resolver Server": "Resolver Server",
|
"Resolver Server": "Resolver Server",
|
||||||
"Resource Record Type": "Resource Record Type",
|
"Resource Record Type": "Resource Record Type",
|
||||||
"Last Result": "Hasil Terakhir",
|
"Last Result": "Hasil Terakhir",
|
||||||
"Create your admin account": "Buat admin akun anda",
|
"Create your admin account": "Buat admin akun Anda",
|
||||||
"Repeat Password": "Ulangi Sandi",
|
"Repeat Password": "Ulangi Sandi",
|
||||||
"Import Backup": "Impor Backup",
|
"Import Backup": "Impor Cadangan",
|
||||||
"Export Backup": "Expor Backup",
|
"Export Backup": "Expor Cadangan",
|
||||||
Export: "Expor",
|
Export: "Expor",
|
||||||
Import: "Impor",
|
Import: "Impor",
|
||||||
respTime: "Tanggapan. Waktu (milidetik)",
|
respTime: "Tanggapan. Waktu (milidetik)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
"Default enabled": "Default diaktifkan",
|
"Default enabled": "Bawaan diaktifkan",
|
||||||
"Apply on all existing monitors": "Terapkan pada semua monitor yang ada",
|
"Apply on all existing monitors": "Terapkan pada semua monitor yang ada",
|
||||||
Create: "Buat",
|
Create: "Buat",
|
||||||
"Clear Data": "Bersihkan Data",
|
"Clear Data": "Bersihkan Data",
|
||||||
Events: "Event",
|
Events: "Peristiwa",
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Heartbeats",
|
||||||
"Auto Get": "Auto Get",
|
"Auto Get": "Ambil Otomatis",
|
||||||
backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam file JSON.",
|
backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam berkas JSON.",
|
||||||
backupDescription2: "Catatan: Data sejarah dan event tidak disertakan.",
|
backupDescription2: "Catatan: Data sejarah dan peristiwa tidak disertakan.",
|
||||||
backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam file ekspor, harap simpan dengan hati-hati.",
|
backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam berkas ekspor, harap simpan dengan hati-hati.",
|
||||||
alertNoFile: "Silakan pilih file untuk diimpor.",
|
alertNoFile: "Silakan pilih berkas untuk diimpor.",
|
||||||
alertWrongFileType: "Silakan pilih file JSON .",
|
alertWrongFileType: "Silakan pilih berkas JSON.",
|
||||||
"Clear all statistics": "Hapus semua statistik",
|
"Clear all statistics": "Hapus semua statistik",
|
||||||
"Skip existing": "Lewati yang ada",
|
"Skip existing": "Lewati yang ada",
|
||||||
Overwrite: "Timpa",
|
Overwrite: "Timpa",
|
||||||
|
@ -145,29 +145,29 @@ export default {
|
||||||
"Setup 2FA": "Pengaturan 2FA",
|
"Setup 2FA": "Pengaturan 2FA",
|
||||||
"Enable 2FA": "Aktifkan 2FA",
|
"Enable 2FA": "Aktifkan 2FA",
|
||||||
"Disable 2FA": "Nonaktifkan 2FA",
|
"Disable 2FA": "Nonaktifkan 2FA",
|
||||||
"2FA Settings": "Settings 2FA",
|
"2FA Settings": "Pengaturan 2FA",
|
||||||
"Two Factor Authentication": "Otentikasi Dua Faktor",
|
"Two Factor Authentication": "Autentikasi Dua Faktor",
|
||||||
Active: "Aktif",
|
Active: "Aktif",
|
||||||
Inactive: "Tidak Aktif",
|
Inactive: "Tidak Aktif",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Lihat URI",
|
"Show URI": "Lihat URI",
|
||||||
Tags: "Tag",
|
Tags: "Tanda",
|
||||||
"Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
|
"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 name already exist.": "Tanda dengan nama ini sudah ada.",
|
||||||
"Tag with this value already exist.": "Tag dengan nilai ini sudah ada.",
|
"Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
|
||||||
color: "warna",
|
color: "warna",
|
||||||
"value (optional)": "nilai (harus diisi)",
|
"value (optional)": "nilai (harus diisi)",
|
||||||
Gray: "Abu Abu",
|
Gray: "Abu-abu",
|
||||||
Red: "Merah",
|
Red: "Merah",
|
||||||
Orange: "Oranye",
|
Orange: "Jingga",
|
||||||
Green: "Hijau",
|
Green: "Hijau",
|
||||||
Blue: "Biru",
|
Blue: "Biru",
|
||||||
Indigo: "Indigo",
|
Indigo: "Biru Tua",
|
||||||
Purple: "Ungu",
|
Purple: "Ungu",
|
||||||
Pink: "Merah Muda",
|
Pink: "Merah Muda",
|
||||||
"Search...": "Cari...",
|
"Search...": "Cari...",
|
||||||
"Avg. Ping": "Rata-rata. Ping",
|
"Avg. Ping": "Rata-rata Ping",
|
||||||
"Avg. Response": "Rata-rata. Respon",
|
"Avg. Response": "Rata-rata Tanggapan",
|
||||||
"Entry Page": "Halaman Masuk",
|
"Entry Page": "Halaman Masuk",
|
||||||
statusPageNothing: "Tidak ada di sini, silakan tambahkan grup atau monitor.",
|
statusPageNothing: "Tidak ada di sini, silakan tambahkan grup atau monitor.",
|
||||||
"No Services": "Tidak ada Layanan",
|
"No Services": "Tidak ada Layanan",
|
||||||
|
@ -177,7 +177,7 @@ export default {
|
||||||
"Add Group": "Tambah Grup",
|
"Add Group": "Tambah Grup",
|
||||||
"Add a monitor": "Tambah monitor",
|
"Add a monitor": "Tambah monitor",
|
||||||
"Edit Status Page": "Edit Halaman Status",
|
"Edit Status Page": "Edit Halaman Status",
|
||||||
"Go to Dashboard": "Lihat Dashboard",
|
"Go to Dashboard": "Pergi ke Dasbor",
|
||||||
"Status Page": "Halaman Status",
|
"Status Page": "Halaman Status",
|
||||||
// Start notification form
|
// Start notification form
|
||||||
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
||||||
|
@ -194,22 +194,22 @@ export default {
|
||||||
"webhook": "Webhook",
|
"webhook": "Webhook",
|
||||||
"Post URL": "Post URL",
|
"Post URL": "Post URL",
|
||||||
"Content Type": "Tipe konten",
|
"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}",
|
webhookFormDataDesc: "{multipart} bagus untuk PHP, Anda hanya perlu mengurai json dengan {decodeFunction}",
|
||||||
"smtp": "Email (SMTP)",
|
"smtp": "Surel (SMTP)",
|
||||||
secureOptionNone: "None / STARTTLS (25, 587)",
|
secureOptionNone: "None / STARTTLS (25, 587)",
|
||||||
secureOptionTLS: "TLS (465)",
|
secureOptionTLS: "TLS (465)",
|
||||||
"Ignore TLS Error": "Ignore TLS Error",
|
"Ignore TLS Error": "Abaikan Kesalahan TLS",
|
||||||
"From Email": "From Email",
|
"From Email": "Dari Surel",
|
||||||
"To Email": "To Email",
|
"To Email": "Ke Surel",
|
||||||
smtpCC: "CC",
|
smtpCC: "CC",
|
||||||
smtpBCC: "BCC",
|
smtpBCC: "BCC",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"Discord Webhook URL": "Discord Webhook URL",
|
"Discord Webhook URL": "Discord Webhook URL",
|
||||||
wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook",
|
wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook",
|
||||||
"Bot Display Name": "Nama Bot",
|
"Bot Display Name": "Nama Bot",
|
||||||
"Prefix Custom Message": "Prefix Pesan",
|
"Prefix Custom Message": "Awalan Pesan",
|
||||||
"Hello @everyone is...": "Hallo {'@'}everyone is...",
|
"Hello @everyone is...": "Halo {'@'}everyone is...",
|
||||||
"teams": "Microsoft Teams",
|
"teams": "Microsoft Teams",
|
||||||
"Webhook URL": "Webhook URL",
|
"Webhook URL": "Webhook URL",
|
||||||
wayToGetTeamsURL: "Anda dapat mempelajari cara membuat url webhook {0}.",
|
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!",
|
signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!",
|
||||||
"gotify": "Gotify",
|
"gotify": "Gotify",
|
||||||
"Application Token": "Token Aplikasi",
|
"Application Token": "Token Aplikasi",
|
||||||
"Server URL": "Server URL",
|
"Server URL": "URL Peladen",
|
||||||
"Priority": "Prioritas",
|
"Priority": "Prioritas",
|
||||||
"slack": "Slack",
|
"slack": "Slack",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Ikon Emoji",
|
||||||
"Channel Name": "Nama Channel",
|
"Channel Name": "Nama Saluran",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
aboutWebhooks: "Info lain tentang webhook: {0}",
|
aboutWebhooks: "Info lain tentang webhook: {0}",
|
||||||
aboutChannelName: "Masukan nama channel di {0} Kolom Nama Channel jika Anda ingin melewati channel webhook. Contoh: #other-channel",
|
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 default ke halaman Project Github.",
|
aboutKumaURL: "Jika Anda membiarkan bidang URL Uptime Kuma kosong, itu akan menjadi bawaan ke halaman Proyek Github.",
|
||||||
emojiCheatSheet: "Emoji cheat sheet: {0}",
|
emojiCheatSheet: "Lembar contekan emoji: {0}",
|
||||||
"rocket.chat": "Rocket.chat",
|
"rocket.chat": "Rocket.chat",
|
||||||
pushover: "Pushover",
|
pushover: "Pushover",
|
||||||
pushy: "Pushy",
|
pushy: "Pushy",
|
||||||
|
@ -246,7 +246,7 @@ export default {
|
||||||
"Message Title": "Judul Pesan",
|
"Message Title": "Judul Pesan",
|
||||||
"Notification Sound": "Suara Nofifikasi",
|
"Notification Sound": "Suara Nofifikasi",
|
||||||
"More info on:": "Info lebih lanjut tentang: {0}",
|
"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.",
|
pushoverDesc2: "Jika Anda ingin mengirim pemberitahuan ke perangkat yang berbeda, isi kolom Perangkat.",
|
||||||
"SMS Type": "Tipe SMS",
|
"SMS Type": "Tipe SMS",
|
||||||
octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)",
|
octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)",
|
||||||
|
@ -262,24 +262,24 @@ export default {
|
||||||
"Read more": "Baca lebih lajut",
|
"Read more": "Baca lebih lajut",
|
||||||
appriseInstalled: "Apprise diinstall.",
|
appriseInstalled: "Apprise diinstall.",
|
||||||
appriseNotInstalled: "Apprise tidak diinstall. {0}",
|
appriseNotInstalled: "Apprise tidak diinstall. {0}",
|
||||||
"Access Token": "Access Token",
|
"Access Token": "Token Akses",
|
||||||
"Channel access token": "Channel access token",
|
"Channel access token": "Token akses saluran",
|
||||||
"Line Developers Console": "Line Developers Console",
|
"Line Developers Console": "Konsol Pengembang Line",
|
||||||
lineDevConsoleTo: "Line Developers Console - {0}",
|
lineDevConsoleTo: "Konsol Pengembang Line - {0}",
|
||||||
"Basic Settings": "Pengaturan dasar",
|
"Basic Settings": "Pengaturan Dasar",
|
||||||
"User ID": "User ID",
|
"User ID": "ID User",
|
||||||
"Messaging API": "Messaging API",
|
"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.",
|
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",
|
"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.",
|
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 channel default tujuan posting webhook dengan memasukkan nama channel ke dalam Kolom \"Channel Name\". Ini perlu diaktifkan di pengaturan webhook Mattermost. contoh: #other-channel",
|
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",
|
"matrix": "Matrix",
|
||||||
promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
|
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.",
|
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.",
|
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).",
|
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)",
|
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",
|
"Feishu WebHookUrl": "Feishu WebHookUrl",
|
||||||
// End notification form
|
// End notification form
|
||||||
};
|
};
|
||||||
|
|
|
@ -184,7 +184,7 @@ export default {
|
||||||
Required: "필수",
|
Required: "필수",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
"Bot Token": "봇 토큰",
|
"Bot Token": "봇 토큰",
|
||||||
"You can get a token from": "토큰은 여기서 얻을 수 있어요:",
|
wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
|
||||||
"Chat ID": "채팅 ID",
|
"Chat ID": "채팅 ID",
|
||||||
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
|
||||||
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
|
||||||
|
|
|
@ -185,7 +185,7 @@ export default {
|
||||||
"Required": "Obligatorisk",
|
"Required": "Obligatorisk",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"Bot Token": "Bot Token",
|
"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",
|
"Chat ID": "Chat ID",
|
||||||
supportTelegramChatID: "Support Direct Chat / Group / Channel's 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:",
|
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",
|
pushbullet: "Pushbullet",
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
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",
|
"Required": "Wymagane",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"Bot Token": "Token Bota",
|
"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",
|
"Chat ID": "Identyfikator Czatu",
|
||||||
supportTelegramChatID: "Czat wsprarcia technicznego / Bezpośrednia Rozmowa / Czat Grupowy",
|
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:",
|
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",
|
Events: "Olaylar",
|
||||||
Heartbeats: "Sağlık Durumları",
|
Heartbeats: "Sağlık Durumları",
|
||||||
"Auto Get": "Otomatik Al",
|
"Auto Get": "Otomatik Al",
|
||||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
retryCheckEverySecond: "{0} Saniyede bir dene.",
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
|
||||||
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.",
|
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: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
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: "Please type in your token to verify that 2FA is working",
|
twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
|
||||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
|
||||||
"Import Backup": "Import Backup",
|
"Import Backup": "Yedeği içe aktar",
|
||||||
"Export Backup": "Export Backup",
|
"Export Backup": "Yedeği dışa aktar",
|
||||||
Export: "Export",
|
Export: "Dışa aktar",
|
||||||
Import: "Import",
|
Import: "İçe aktar",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Varsayılan etkinleştirilmiş",
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
"Apply on all existing monitors": "Var olan bütün servislere uygula",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.",
|
||||||
alertNoFile: "Please select a file to import.",
|
alertNoFile: "İçeri aktarmak için bir dosya seçiniz.",
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
alertWrongFileType: "Lütfen bir JSON dosyası seçiniz.",
|
||||||
"Clear all statistics": "Clear all Statistics",
|
"Clear all statistics": "Bütün istatistikleri temizle",
|
||||||
"Skip existing": "Skip existing",
|
"Skip existing": "Var olanı atla",
|
||||||
Overwrite: "Overwrite",
|
Overwrite: "Üzerine yaz",
|
||||||
Options: "Options",
|
Options: "Seçenekler",
|
||||||
"Keep both": "Keep both",
|
"Keep both": "İkisini sakla",
|
||||||
"Verify Token": "Verify Token",
|
"Verify Token": "Tokeni doğrula",
|
||||||
"Setup 2FA": "Setup 2FA",
|
"Setup 2FA": "2FA Kur",
|
||||||
"Enable 2FA": "Enable 2FA",
|
"Enable 2FA": "2FA Etkinleştir",
|
||||||
"Disable 2FA": "Disable 2FA",
|
"Disable 2FA": "2FA Devre dışı bırak",
|
||||||
"2FA Settings": "2FA Settings",
|
"2FA Settings": "2FA Ayarları",
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||||
Active: "Active",
|
Active: "Aktif",
|
||||||
Inactive: "Inactive",
|
Inactive: "İnaktif",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "Show URI",
|
"Show URI": "Show URI",
|
||||||
Tags: "Tags",
|
Tags: "Tags",
|
||||||
|
|
|
@ -22,7 +22,8 @@ export default {
|
||||||
Appearance: "外观设置",
|
Appearance: "外观设置",
|
||||||
Theme: "主题",
|
Theme: "主题",
|
||||||
General: "基本设置",
|
General: "基本设置",
|
||||||
Version: "Version",
|
"Primary Base URL": "站点地址(URL)",
|
||||||
|
Version: "版本",
|
||||||
"Check Update On GitHub": "检查更新",
|
"Check Update On GitHub": "检查更新",
|
||||||
List: "列表",
|
List: "列表",
|
||||||
Add: "添加",
|
Add: "添加",
|
||||||
|
@ -43,7 +44,7 @@ export default {
|
||||||
Delete: "删除",
|
Delete: "删除",
|
||||||
Current: "当前",
|
Current: "当前",
|
||||||
Uptime: "可用率",
|
Uptime: "可用率",
|
||||||
"Cert Exp.": "证书过期",
|
"Cert Exp.": "证书有效期",
|
||||||
days: "天",
|
days: "天",
|
||||||
day: "天",
|
day: "天",
|
||||||
"-day": " 天",
|
"-day": " 天",
|
||||||
|
@ -63,6 +64,9 @@ export default {
|
||||||
"Upside Down Mode": "反向监控",
|
"Upside Down Mode": "反向监控",
|
||||||
"Max. Redirects": "重定向次数",
|
"Max. Redirects": "重定向次数",
|
||||||
"Accepted Status Codes": "有效状态码",
|
"Accepted Status Codes": "有效状态码",
|
||||||
|
"Push URL": "推送链接",
|
||||||
|
needPushEvery: "你需要每 {0} 秒调用一次。",
|
||||||
|
pushOptionalParams: "可选参数:{0}",
|
||||||
Save: "保存",
|
Save: "保存",
|
||||||
Notifications: "消息通知",
|
Notifications: "消息通知",
|
||||||
"Not available, please setup.": "无可用通道,请先设置",
|
"Not available, please setup.": "无可用通道,请先设置",
|
||||||
|
@ -103,10 +107,10 @@ export default {
|
||||||
"Certificate Info": "证书信息",
|
"Certificate Info": "证书信息",
|
||||||
"Resolver Server": "解析服务器",
|
"Resolver Server": "解析服务器",
|
||||||
"Resource Record Type": "资源记录类型",
|
"Resource Record Type": "资源记录类型",
|
||||||
"Last Result": "Last Result",
|
"Last Result": "最后结果",
|
||||||
"Create your admin account": "创建管理员账号",
|
"Create your admin account": "创建管理员账号",
|
||||||
"Repeat Password": "重复密码",
|
"Repeat Password": "重复密码",
|
||||||
respTime: "Resp. Time (ms)",
|
respTime: "响应时间(毫秒)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
Create: "创建",
|
Create: "创建",
|
||||||
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
|
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
|
||||||
|
@ -126,21 +130,21 @@ export default {
|
||||||
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
||||||
alertNoFile: "请选择一个文件导入",
|
alertNoFile: "请选择一个文件导入",
|
||||||
alertWrongFileType: "请选择一个 JSON 格式的文件",
|
alertWrongFileType: "请选择一个 JSON 格式的文件",
|
||||||
twoFAVerifyLabel: "请输入Token以验证2FA(二次验证)是否正常工作",
|
twoFAVerifyLabel: "请输入Token以验证 2FA(二次验证)是否正常工作",
|
||||||
tokenValidSettingsMsg: "Token有效!您现在可以保存2FA(二次验证)设置",
|
tokenValidSettingsMsg: "Token有效!您现在可以保存 2FA(二次验证)设置",
|
||||||
confirmEnableTwoFAMsg: "确定要启用2FA(二次验证)吗?",
|
confirmEnableTwoFAMsg: "确定要启用 2FA(二次验证)吗?",
|
||||||
confirmDisableTwoFAMsg: "确定要禁用2FA(二次验证)吗?",
|
confirmDisableTwoFAMsg: "确定要禁用 2FA(二次验证)吗?",
|
||||||
"Apply on all existing monitors": "应用到所有监控项",
|
"Apply on all existing monitors": "应用到所有监控项",
|
||||||
"Verify Token": "验证Token",
|
"Verify Token": "验证 Token",
|
||||||
"Setup 2FA": "设置2FA",
|
"Setup 2FA": "设置 2FA",
|
||||||
"Enable 2FA": "启用2FA",
|
"Enable 2FA": "启用 2FA",
|
||||||
"Disable 2FA": "禁用2FA",
|
"Disable 2FA": "禁用 2FA",
|
||||||
"2FA Settings": "2FA设置",
|
"2FA Settings": "2FA 设置",
|
||||||
"Two Factor Authentication": "双因素认证",
|
"Two Factor Authentication": "双因素认证",
|
||||||
Active: "有效",
|
Active: "生效",
|
||||||
Inactive: "无效",
|
Inactive: "未生效",
|
||||||
Token: "Token",
|
Token: "Token",
|
||||||
"Show URI": "显示URI",
|
"Show URI": "显示链接",
|
||||||
"Clear all statistics": "清除所有统计数据",
|
"Clear all statistics": "清除所有统计数据",
|
||||||
retryCheckEverySecond: "重试间隔 {0} 秒",
|
retryCheckEverySecond: "重试间隔 {0} 秒",
|
||||||
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
|
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
|
||||||
|
@ -167,7 +171,7 @@ export default {
|
||||||
Purple: "紫色",
|
Purple: "紫色",
|
||||||
Pink: "粉色",
|
Pink: "粉色",
|
||||||
"Search...": "搜索...",
|
"Search...": "搜索...",
|
||||||
"Avg. Ping": "平均Ping",
|
"Avg. Ping": "平均 Ping",
|
||||||
"Avg. Response": "平均响应",
|
"Avg. Response": "平均响应",
|
||||||
"Entry Page": "入口页面",
|
"Entry Page": "入口页面",
|
||||||
statusPageNothing: "这里什么也没有,请添加一个分组或一个监控项。",
|
statusPageNothing: "这里什么也没有,请添加一个分组或一个监控项。",
|
||||||
|
@ -182,7 +186,7 @@ export default {
|
||||||
"Status Page": "状态页",
|
"Status Page": "状态页",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "电子邮件(SMTP)",
|
||||||
discord: "Discord",
|
discord: "Discord",
|
||||||
teams: "Microsoft Teams",
|
teams: "Microsoft Teams",
|
||||||
signal: "Signal",
|
signal: "Signal",
|
||||||
|
@ -194,9 +198,97 @@ export default {
|
||||||
octopush: "Octopush",
|
octopush: "Octopush",
|
||||||
promosms: "PromoSMS",
|
promosms: "PromoSMS",
|
||||||
lunasea: "LunaSea",
|
lunasea: "LunaSea",
|
||||||
apprise: "Apprise (Support 50+ Notification services)",
|
apprise: "Apprise (支持50+种通知服务)",
|
||||||
pushbullet: "Pushbullet",
|
pushbullet: "Pushbullet",
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
"Feishu WebHookUrl": "飞书 WebHook 地址",
|
"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>
|
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
<option value="push">
|
<option value="push">
|
||||||
Push
|
Push
|
||||||
</option>
|
</option>
|
||||||
|
<option value="steam">
|
||||||
|
Steam Game Server
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,11 +49,11 @@
|
||||||
|
|
||||||
<!-- Push URL -->
|
<!-- Push URL -->
|
||||||
<div v-if="monitor.type === 'push' " class="my-3">
|
<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" />
|
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
You should call this url every {{ monitor.interval }} seconds.<br />
|
{{ $t("needPushEvery", [monitor.interval]) }}<br />
|
||||||
Optional parameters: msg, ping
|
{{ $t("pushOptionalParams", ["msg, ping"]) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -63,18 +66,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TCP Port / Ping / DNS only -->
|
<!-- Hostname -->
|
||||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3">
|
<!-- 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>
|
<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>
|
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- For TCP Port Type -->
|
<!-- Port -->
|
||||||
<div v-if="monitor.type === 'port' " class="my-3">
|
<!-- 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>
|
<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">
|
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DNS Resolver Server -->
|
||||||
<!-- For DNS Type -->
|
<!-- For DNS Type -->
|
||||||
<template v-if="monitor.type === 'dns'">
|
<template v-if="monitor.type === 'dns'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
|
@ -140,7 +146,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||||
<label class="form-check-label" for="upside-down">
|
<label class="form-check-label" for="upside-down">
|
||||||
{{ $t("Upside Down Mode") }}
|
{{ $t("Upside Down Mode") }}
|
||||||
|
@ -195,6 +201,7 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div v-if="$root.isMobile" class="mt-3" />
|
<div v-if="$root.isMobile" class="mt-3" />
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
|
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
|
||||||
<p v-if="$root.notificationList.length === 0">
|
<p v-if="$root.notificationList.length === 0">
|
||||||
{{ $t("Not available, please setup.") }}
|
{{ $t("Not available, please setup.") }}
|
||||||
|
@ -214,6 +221,51 @@
|
||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||||
{{ $t("Setup Notification") }}
|
{{ $t("Setup Notification") }}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -285,6 +337,14 @@ export default {
|
||||||
|
|
||||||
pushURL() {
|
pushURL() {
|
||||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
|
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) {
|
"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) {
|
if (this.monitor.retryInterval === oldValue) {
|
||||||
this.monitor.retryInterval = value;
|
this.monitor.retryInterval = value;
|
||||||
}
|
}
|
||||||
|
@ -349,6 +409,7 @@ export default {
|
||||||
type: "http",
|
type: "http",
|
||||||
name: "",
|
name: "",
|
||||||
url: "https://",
|
url: "https://",
|
||||||
|
method: "GET",
|
||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: this.interval,
|
retryInterval: this.interval,
|
||||||
maxretries: 0,
|
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() {
|
async submit() {
|
||||||
this.processing = true;
|
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) {
|
if (this.isAdd) {
|
||||||
this.$root.add(this.monitor, async (res) => {
|
this.$root.add(this.monitor, async (res) => {
|
||||||
|
|
||||||
|
@ -422,8 +517,12 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
|
|
||||||
<!-- Primary Base URL -->
|
<!-- Primary Base URL -->
|
||||||
<div class="mb-4">
|
<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">
|
<div class="input-group mb-3">
|
||||||
<input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+">
|
<input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+">
|
||||||
|
@ -119,6 +119,25 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
|
@ -334,9 +353,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'id-ID' ">
|
<template v-else-if="$i18n.locale === 'id-ID' ">
|
||||||
<p> Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>? </p>
|
<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>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>
|
<p>Gunakan dengan hati-hati.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
<template v-else-if="$i18n.locale === 'ru-RU' ">
|
||||||
|
@ -477,6 +496,10 @@ export default {
|
||||||
this.settings.entryPage = "dashboard";
|
this.settings.entryPage = "dashboard";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.settings.keepDataPeriodDays === undefined) {
|
||||||
|
this.settings.keepDataPeriodDays = 180;
|
||||||
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const { genSecret } = require("../src/util");
|
const { genSecret, sleep } = require("../src/util");
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test genSecret", () => {
|
describe("Test genSecret", () => {
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
it("should be correct length", () => {
|
it("should be correct length", () => {
|
||||||
let secret = genSecret(-1);
|
let secret = genSecret(-1);
|
||||||
expect(secret).toEqual("");
|
expect(secret).toEqual("");
|
||||||
|
@ -34,4 +34,11 @@ describe("Test genSecret", () => {
|
||||||
expect(secret).toContain("A");
|
expect(secret).toContain("A");
|
||||||
expect(secret).toContain("9");
|
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,
|
"compileOnSave": true,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"newLine": "LF",
|
||||||
"target": "es2018",
|
"target": "es2018",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|
Loading…
Reference in a new issue