mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 23:04:04 +00:00
Merge branch 'master' into dns-monitor
This commit is contained in:
commit
46ac753c46
30 changed files with 1571 additions and 3972 deletions
|
@ -16,6 +16,9 @@ module.exports = {
|
|||
requireConfigFile: false,
|
||||
},
|
||||
rules: {
|
||||
"camelcase": ["warn", {
|
||||
"properties": "never"
|
||||
}],
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
"no-unused-vars": "warn",
|
||||
|
@ -74,5 +77,6 @@ module.exports = {
|
|||
"no-empty": ["error", {
|
||||
"allowEmptyCatch": true
|
||||
}],
|
||||
"no-control-regex": "off"
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
{
|
||||
"extends": "stylelint-config-recommended"
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"no-descending-specificity": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"declaration-empty-line-before": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,38 @@ The project was created with vite.js (vue3). Then I created a sub-directory call
|
|||
|
||||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||
|
||||
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
|
||||
# Can I create a pull request for Uptime Kuma?
|
||||
|
||||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
|
||||
|
||||
If you are not sure, feel free to create an empty pull request draft first.
|
||||
|
||||
## Pull Request Examples
|
||||
|
||||
### ✅ High - Medium Priority
|
||||
|
||||
- Add a new notification
|
||||
- Add a chart
|
||||
- Fix a bug
|
||||
|
||||
### *️⃣ Requires one more reviewer
|
||||
|
||||
I do not have such knowledge to test it
|
||||
|
||||
- Add k8s supports
|
||||
|
||||
### *️⃣ Low Priority
|
||||
|
||||
It chnaged my current workflow and require further studies.
|
||||
|
||||
- Change my release approach
|
||||
|
||||
### ❌ Won't Merge
|
||||
|
||||
- Duplicated pull request
|
||||
- Buggy
|
||||
- Existing logic is completely modified or deleted
|
||||
- A function that is completely out of scope
|
||||
|
||||
# Project Styles
|
||||
|
||||
|
@ -19,16 +50,27 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
|
|||
- All settings in frontend.
|
||||
- Easy to use
|
||||
|
||||
# Coding Styles
|
||||
|
||||
- Follow .editorconfig
|
||||
- Follow eslint
|
||||
|
||||
## Name convention
|
||||
|
||||
- Javascript/Typescript: camelCaseType
|
||||
- SQLite: underscore_type
|
||||
- CSS/SCSS: dash-type
|
||||
|
||||
# Tools
|
||||
- Node.js >= 14
|
||||
- Git
|
||||
- IDE that supports .editorconfig (I am using Intellji Idea)
|
||||
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
|
||||
- A SQLite tool (I am using SQLite Expert Personal)
|
||||
|
||||
# Prepare the dev
|
||||
# Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm install --dev
|
||||
```
|
||||
|
||||
# Backend Dev
|
||||
|
@ -39,7 +81,6 @@ npm run start-server
|
|||
# Or
|
||||
|
||||
node server/server.js
|
||||
|
||||
```
|
||||
|
||||
It binds to 0.0.0.0:3001 by default.
|
||||
|
@ -92,7 +133,8 @@ The data and socket logic in "src/mixins/socket.js"
|
|||
|
||||
# Database Migration
|
||||
|
||||
TODO
|
||||
1. create `patch{num}.sql` in `./db/`
|
||||
1. update `latestVersion` in `./server/database.js`
|
||||
|
||||
# Unit Test
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
||||
# split the sqlite install here, so that it can caches the prebuilt
|
||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \
|
||||
npm install better-sqlite3@7.4.3 bcrypt@5.0.1 && \
|
||||
apk del .build-deps && \
|
||||
rm -f /usr/bin/python
|
||||
|
||||
|
|
4507
package-lock.json
generated
4507
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
@ -10,6 +10,9 @@
|
|||
"node": "14.*"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
|
@ -19,7 +22,7 @@
|
|||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.3.2 --target release . --push",
|
||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"setup": "git checkout 1.3.2 && npm install && npm run build",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
|
@ -36,11 +39,11 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||
"@louislam/sqlite3": "^5.0.3",
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"better-sqlite3": "^7.4.3",
|
||||
"bootstrap": "^5.1.0",
|
||||
"chart.js": "^3.5.0",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
|
@ -56,7 +59,7 @@
|
|||
"password-hash": "^1.2.2",
|
||||
"prom-client": "^13.2.0",
|
||||
"prometheus-api-metrics": "^3.2.0",
|
||||
"redbean-node": "0.0.21",
|
||||
"redbean-node": "0.1.1",
|
||||
"socket.io": "^4.1.3",
|
||||
"socket.io-client": "^4.1.3",
|
||||
"tcp-ping": "^0.1.1",
|
||||
|
@ -64,6 +67,7 @@
|
|||
"vue": "^3.2.2",
|
||||
"vue-chart-3": "^0.5.7",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-i18n": "^9.1.7",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
"vue-router": "^4.0.11",
|
||||
"vue-toastification": "^2.0.0-rc.1"
|
||||
|
@ -79,7 +83,6 @@
|
|||
"eslint-plugin-vue": "^7.16.0",
|
||||
"sass": "^1.37.5",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-recommended": "^5.0.0",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"typescript": "^4.3.5",
|
||||
"vite": "^2.4.4"
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
const fs = require("fs");
|
||||
const { sleep, debug, isDev } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const knex = require("knex");
|
||||
const sqlite3 = require("@louislam/sqlite3");
|
||||
|
||||
class Database {
|
||||
|
||||
|
@ -14,48 +11,23 @@ class Database {
|
|||
static sqliteInstance = null;
|
||||
|
||||
static async connect() {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
if (! this.sqliteInstance) {
|
||||
this.sqliteInstance = new sqlite3.Database(Database.path);
|
||||
this.sqliteInstance.run("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
R.useBetterSQLite3 = true;
|
||||
R.betterSQLite3Options.timeout = acquireConnectionTimeout;
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => sqlite3;
|
||||
|
||||
if (isDev) {
|
||||
Dialect.prototype.acquireConnectionOrg = Dialect.prototype.acquireConnection;
|
||||
|
||||
Dialect.prototype.acquireConnection = async function () {
|
||||
let a = await this.acquireConnectionOrg();
|
||||
debug("acquired Connection");
|
||||
return a;
|
||||
};
|
||||
}
|
||||
|
||||
// Always return same connection.
|
||||
Dialect.prototype.acquireRawConnection = async function () {
|
||||
return Database.sqliteInstance;
|
||||
};
|
||||
|
||||
Dialect.prototype.destroyRawConnection = async () => { }
|
||||
|
||||
const knexInstance = knex({
|
||||
client: Dialect,
|
||||
connection: { }, // Do not remove, Leave it empty is ok
|
||||
R.setup("sqlite", {
|
||||
filename: Database.path,
|
||||
useNullAsDefault: true,
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 30000,
|
||||
}
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
}, {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
});
|
||||
|
||||
console.log( knexInstance.pool)
|
||||
console.log("pool size")
|
||||
|
||||
R.setup(knexInstance);
|
||||
|
||||
if (process.env.SQL_LOG === "1") {
|
||||
R.debug(true);
|
||||
}
|
||||
|
@ -63,6 +35,10 @@ class Database {
|
|||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
// Change to WAL
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
}
|
||||
|
||||
static async patch() {
|
||||
|
@ -148,11 +124,18 @@ class Database {
|
|||
return statement !== "";
|
||||
})
|
||||
|
||||
// Use better-sqlite3 to run, prevent "This statement does not return data. Use run() instead"
|
||||
const db = await this.getBetterSQLite3Database();
|
||||
|
||||
for (let statement of statements) {
|
||||
await R.exec(statement);
|
||||
db.prepare(statement).run();
|
||||
}
|
||||
}
|
||||
|
||||
static getBetterSQLite3Database() {
|
||||
return R.knex.client.acquireConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
|
|
|
@ -112,10 +112,9 @@ class Monitor extends BeanModel {
|
|||
|
||||
try {
|
||||
if (this.type === "http" || this.type === "keyword") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
// Use Custom agent to disable session reuse
|
||||
// https://github.com/nodejs/node/issues/3940
|
||||
let res = await axios.get(this.url, {
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
|
@ -123,7 +122,7 @@ class Monitor extends BeanModel {
|
|||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0,
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
}),
|
||||
maxRedirects: this.maxredirects,
|
||||
|
@ -276,7 +275,8 @@ class Monitor extends BeanModel {
|
|||
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.error("Cannot send notification to " + notification.name);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -293,22 +293,22 @@ class Monitor extends BeanModel {
|
|||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||
}
|
||||
|
||||
prometheus.update(bean, tlsInfo)
|
||||
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
|
||||
await R.store(bean)
|
||||
Monitor.sendStats(io, this.id, this.user_id)
|
||||
|
||||
await R.store(bean);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
|
||||
}
|
||||
|
||||
beat();
|
||||
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -279,6 +279,116 @@ class Notification {
|
|||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "mattermost") {
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
}
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel;
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
if (heartbeatJSON["status"] == 0) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushover") {
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||
try {
|
||||
|
@ -404,7 +514,7 @@ class Notification {
|
|||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"Test Successful!"
|
||||
"text": "Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -415,7 +525,7 @@ class Notification {
|
|||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -426,7 +536,7 @@ class Notification {
|
|||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ debug("Importing express");
|
|||
const express = require("express");
|
||||
debug("Importing socket.io");
|
||||
const { Server } = require("socket.io");
|
||||
debug("Importing dayjs");
|
||||
const dayjs = require("dayjs");
|
||||
debug("Importing redbean-node");
|
||||
const { R } = require("redbean-node");
|
||||
debug("Importing jsonwebtoken");
|
||||
|
@ -150,7 +148,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||
if (user) {
|
||||
debug("afterLogin")
|
||||
|
||||
await afterLogin(socket, user)
|
||||
afterLogin(socket, user)
|
||||
|
||||
debug("afterLogin ok")
|
||||
|
||||
|
@ -178,7 +176,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||
let user = await login(data.username, data.password)
|
||||
|
||||
if (user) {
|
||||
await afterLogin(socket, user)
|
||||
afterLogin(socket, user)
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
|
@ -563,7 +561,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
|||
debug("check auto login")
|
||||
if (await setting("disableAuth")) {
|
||||
console.log("Disabled Auth: auto login to admin")
|
||||
await afterLogin(socket, await R.findOne("user"))
|
||||
afterLogin(socket, await R.findOne("user"))
|
||||
socket.emit("autoLogin")
|
||||
} else {
|
||||
debug("need auth")
|
||||
|
@ -623,6 +621,8 @@ async function sendMonitorList(socket) {
|
|||
}
|
||||
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
let list = await R.find("notification", " user_id = ? ", [
|
||||
socket.userID,
|
||||
|
@ -633,6 +633,9 @@ async function sendNotificationList(socket) {
|
|||
}
|
||||
|
||||
io.to(socket.userID).emit("notificationList", result)
|
||||
|
||||
timeLogger.print("Send Notification List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
@ -641,24 +644,27 @@ async function afterLogin(socket, user) {
|
|||
socket.join(user.id)
|
||||
|
||||
let monitorList = await sendMonitorList(socket)
|
||||
|
||||
sendNotificationList(socket)
|
||||
|
||||
// Delay a bit, so that it let the main page to query the data first, since SQLite can process one sql at the same time only.
|
||||
// For example, query the edit data first.
|
||||
setTimeout(async () => {
|
||||
for (let monitorID in monitorList) {
|
||||
sendHeartbeatList(socket, monitorID);
|
||||
sendImportantHeartbeatList(socket, monitorID);
|
||||
Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
}, 500);
|
||||
await sleep(500);
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendImportantHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let monitorList = await R.find("monitor", " user_id = ? ", [
|
||||
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||
userID,
|
||||
])
|
||||
|
||||
|
@ -788,6 +794,8 @@ async function sendHeartbeatList(socket, monitorID) {
|
|||
}
|
||||
|
||||
socket.emit("heartbeatList", monitorID, result)
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`)
|
||||
}
|
||||
|
||||
async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app {
|
||||
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -18,7 +18,7 @@ h2 {
|
|||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #CCC;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ h2 {
|
|||
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.dark & {
|
||||
box-shadow: 0 15px 70px rgb(0 0 0);
|
||||
|
@ -41,10 +41,9 @@ h2 {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.shadow-box {
|
||||
//overflow: hidden; // Forget why add this, but multiple select hide by this
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
|
@ -80,9 +79,56 @@ h2 {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
.shadow-box {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
margin-top: 0 !important;
|
||||
padding: 4px 10px !important;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
td:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td:nth-child(-n+3) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid $dark-font-color;
|
||||
display: block;
|
||||
padding: 4px;
|
||||
|
||||
.badge {
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Theme override here
|
||||
.dark {
|
||||
background-color: #090C10;
|
||||
background-color: #090c10;
|
||||
color: $dark-font-color;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
|
@ -124,7 +170,7 @@ h2 {
|
|||
}
|
||||
|
||||
.table-hover > tbody > tr:hover {
|
||||
--bs-table-accent-bg: #070A10;
|
||||
--bs-table-accent-bg: #070a10;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
|
@ -192,6 +238,20 @@ h2 {
|
|||
.multiselect__option--selected {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
tbody {
|
||||
.shadow-box {
|
||||
background-color: $dark-bg2;
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -200,11 +260,13 @@ h2 {
|
|||
|
||||
// page-change
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.20s $easing-in;
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.20s $easing-in;
|
||||
transition: all 0.2s $easing-in;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(50px);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
$primary: #5CDD8B;
|
||||
$danger: #DC3545;
|
||||
$primary: #5cdd8b;
|
||||
$danger: #dc3545;
|
||||
$warning: #f8a306;
|
||||
$link-color: #111;
|
||||
$border-radius: 50rem;
|
||||
|
@ -9,10 +9,10 @@ $highlight-white: #e7faec;
|
|||
|
||||
$dark-font-color: #b1b8c0;
|
||||
$dark-font-color2: #020b05;
|
||||
$dark-bg: #0D1117;
|
||||
$dark-bg2: #070A10;
|
||||
$dark-bg: #0d1117;
|
||||
$dark-bg2: #070a10;
|
||||
$dark-border-color: #1d2634;
|
||||
|
||||
$easing-in: cubic-bezier(0.54,0.78,0.55,0.97);
|
||||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
Confirm
|
||||
{{ $t("Confirm") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@ export default {
|
|||
},
|
||||
yesText: {
|
||||
type: String,
|
||||
default: "Yes",
|
||||
default: "Yes", // TODO: No idea what to translate this
|
||||
},
|
||||
noText: {
|
||||
type: String,
|
||||
|
|
|
@ -163,10 +163,6 @@ export default {
|
|||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
|
||||
.dark & {
|
||||
background-color: #d0d3d5;
|
||||
}
|
||||
}
|
||||
|
||||
&.down {
|
||||
|
@ -186,7 +182,7 @@ export default {
|
|||
}
|
||||
|
||||
.dark {
|
||||
.hp-bar-big .beat.empty{
|
||||
.hp-bar-big .beat.empty {
|
||||
background-color: #848484;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
<div class="form-floating">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||
<label for="floatingInput">Username</label>
|
||||
<label for="floatingInput">{{ $t("Username") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||
<label for="floatingPassword">Password</label>
|
||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||
|
@ -19,12 +19,12 @@
|
|||
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||
|
||||
<label class="form-check-label" for="remember">
|
||||
Remember me
|
||||
{{ $t("Remember me") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
|
||||
Login
|
||||
{{ $t("Login") }}
|
||||
</button>
|
||||
|
||||
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="shadow-box list mb-4">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
Setup Notification
|
||||
{{ $t("Setup Notification") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Notification Type</label>
|
||||
<label for="type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="type" v-model="notification.type" class="form-select">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">Email (SMTP)</option>
|
||||
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="signal">Signal</option>
|
||||
<option value="gotify">Gotify</option>
|
||||
|
@ -27,11 +27,12 @@
|
|||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
<option value="line">Line Messenger</option>
|
||||
<option value="mattermost">Mattermost</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
|
@ -210,7 +211,7 @@
|
|||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||
|
@ -221,7 +222,7 @@
|
|||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
|
@ -238,6 +239,39 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'mattermost'">
|
||||
<div class="mb-3">
|
||||
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||
<label for="mattermost-username" class="form-label">Username</label>
|
||||
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
|
||||
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
|
||||
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
|
||||
<label for="mattermost-channel" class="form-label">Channel Name</label>
|
||||
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushy'">
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
|
@ -288,9 +322,9 @@
|
|||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
||||
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||
|
@ -330,7 +364,7 @@
|
|||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<span style="color: red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
|
@ -366,10 +400,10 @@
|
|||
|
||||
<template v-if="notification.type === 'lunasea'">
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
|
||||
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<p><span style="color:red;"><sup>*</sup></span>Required</p>
|
||||
<p><span style="color: red;"><sup>*</sup></span>Required</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -407,13 +441,13 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
Delete
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||
Test
|
||||
{{ $t("Test") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
Save
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
// Configurable filtering on top of the returned data
|
||||
chartPeriodHrs: 6,
|
||||
};
|
||||
},
|
||||
|
@ -55,11 +56,10 @@ export default {
|
|||
|
||||
elements: {
|
||||
point: {
|
||||
// Hide points on chart unless mouse-over
|
||||
radius: 0,
|
||||
hitRadius: 100,
|
||||
},
|
||||
bar: {
|
||||
barThickness: "flex",
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
|
@ -77,9 +77,9 @@ export default {
|
|||
maxRotation: 0,
|
||||
autoSkipPadding: 30,
|
||||
},
|
||||
bounds: "ticks",
|
||||
grid: {
|
||||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
|
||||
offset: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
|
@ -109,8 +109,11 @@ export default {
|
|||
mode: "nearest",
|
||||
intersect: false,
|
||||
padding: 10,
|
||||
backgroundColor: this.$root.theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)",
|
||||
bodyColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
|
||||
titleColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
|
||||
filter: function (tooltipItem) {
|
||||
return tooltipItem.datasetIndex === 0;
|
||||
return tooltipItem.datasetIndex === 0; // Hide tooltip on Bar Chart
|
||||
},
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
|
@ -125,32 +128,29 @@ export default {
|
|||
}
|
||||
},
|
||||
chartData() {
|
||||
let ping_data = [];
|
||||
let down_data = [];
|
||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
|
||||
if (this.monitorId in this.$root.heartbeatList) {
|
||||
ping_data = this.$root.heartbeatList[this.monitorId]
|
||||
this.$root.heartbeatList[this.monitorId]
|
||||
.filter(
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
|
||||
.map((beat) => {
|
||||
return {
|
||||
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
};
|
||||
});
|
||||
down_data = this.$root.heartbeatList[this.monitorId]
|
||||
.filter(
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
|
||||
.map((beat) => {
|
||||
return {
|
||||
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: beat.status === 0 ? 1 : 0,
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
data: ping_data,
|
||||
// Line Chart
|
||||
data: pingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#5CDD8B",
|
||||
|
@ -158,11 +158,15 @@ export default {
|
|||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
// Bar Chart
|
||||
type: "bar",
|
||||
data: down_data,
|
||||
data: downData,
|
||||
borderColor: "#00000000",
|
||||
backgroundColor: "#DC354568",
|
||||
yAxisID: "y1",
|
||||
barThickness: "flex",
|
||||
barPercentage: 1,
|
||||
categoryPercentage: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -27,18 +27,18 @@ export default {
|
|||
|
||||
text() {
|
||||
if (this.status === 0) {
|
||||
return "Down"
|
||||
return this.$t("Down");
|
||||
}
|
||||
|
||||
if (this.status === 1) {
|
||||
return "Up"
|
||||
return this.$t("Up");
|
||||
}
|
||||
|
||||
if (this.status === 2) {
|
||||
return "Pending"
|
||||
return this.$t("Pending");
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
return this.$t("Unknown");
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
12
src/languages/en.js
Normal file
12
src/languages/en.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
languageName: "English",
|
||||
checkEverySecond: "Check every {0} seconds.",
|
||||
"Avg.": "Avg. ",
|
||||
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
|
||||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
|
||||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
|
||||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
|
||||
passwordNotMatchMsg: "The repeat password does not match.",
|
||||
notificationDescription: "Please assign a notification to monitor(s) to get it to work.",
|
||||
}
|
97
src/languages/zh-HK.js
Normal file
97
src/languages/zh-HK.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
export default {
|
||||
languageName: "繁體中文 (香港)",
|
||||
Settings: "設定",
|
||||
Dashboard: "錶板",
|
||||
"New Update": "有更新",
|
||||
Language: "語言",
|
||||
Appearance: "外觀",
|
||||
Theme: "主題",
|
||||
General: "一般",
|
||||
Version: "版本",
|
||||
"Check Update On GitHub": "到 Github 查看更新",
|
||||
List: "列表",
|
||||
Add: "新增",
|
||||
"Add New Monitor": "新增監測器",
|
||||
"Quick Stats": "綜合數據",
|
||||
Up: "上線",
|
||||
Down: "離線",
|
||||
Pending: "待定",
|
||||
Unknown: "不明",
|
||||
Pause: "暫停",
|
||||
Name: "名稱",
|
||||
Status: "狀態",
|
||||
DateTime: "日期時間",
|
||||
Message: "內容",
|
||||
"No important events": "沒有重要事件",
|
||||
Resume: "恢復",
|
||||
Edit: "編輯",
|
||||
Delete: "刪除",
|
||||
Current: "目前",
|
||||
Uptime: "上線率",
|
||||
"Cert Exp.": "証書期限",
|
||||
days: "日",
|
||||
day: "日",
|
||||
"-day": "日",
|
||||
hour: "小時",
|
||||
"-hour": "小時",
|
||||
checkEverySecond: "每 {0} 秒檢查一次",
|
||||
"Avg.": "平均",
|
||||
Response: "反應時間",
|
||||
Ping: "反應時間",
|
||||
"Monitor Type": "監測器類型",
|
||||
Keyword: "關鍵字",
|
||||
"Friendly Name": "名稱",
|
||||
URL: "網址 URL",
|
||||
Hostname: "Hostname",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "檢查間距",
|
||||
Retries: "重試數次確定為離線",
|
||||
retriesDescription: "重試多少次後才判定為離線及傳送通知。如數值為 0 會即判定為離線及傳送通知。",
|
||||
Advanced: "進階",
|
||||
ignoreTLSError: "忽略 TLS/SSL 錯誤",
|
||||
"Upside Down Mode": "反轉模式",
|
||||
upsideDownModeDescription: "反轉狀態,如網址是可正常瀏覽,會被判定為 '離線/DOWN'",
|
||||
"Max. Redirects": "跟隨重新導向 (Redirect) 的次數",
|
||||
maxRedirectDescription: "設為 0 即不跟蹤",
|
||||
"Accepted Status Codes": "接受為上線的 HTTP 狀態碼",
|
||||
acceptedStatusCodesDescription: "可多選",
|
||||
Save: "儲存",
|
||||
Notifications: "通知",
|
||||
"Not available, please setup.": "無法使用,需要設定",
|
||||
"Setup Notification": "設定通知",
|
||||
Light: "明亮",
|
||||
Dark: "暗黑",
|
||||
Auto: "自動",
|
||||
"Theme - Heartbeat Bar": "監測器列表 狀態條外觀",
|
||||
Normal: "一般",
|
||||
Bottom: "下方",
|
||||
None: "沒有",
|
||||
Timezone: "時區",
|
||||
"Search Engine Visibility": "是否允許搜尋器索引",
|
||||
"Allow indexing": "允許索引",
|
||||
"Discourage search engines from indexing site": "不建議搜尋器索引",
|
||||
"Change Password": "變更密碼",
|
||||
"Current Password": "目前密碼",
|
||||
"New Password": "新密碼",
|
||||
"Repeat New Password": "確認新密碼",
|
||||
passwordNotMatchMsg: "密碼不一致",
|
||||
"Update Password": "更新密碼",
|
||||
"Disable Auth": "取消登入認証",
|
||||
"Enable Auth": "開啟登入認証",
|
||||
Logout: "登出",
|
||||
notificationDescription: "新增後,你需要在監測器裡啟用。",
|
||||
Leave: "離開",
|
||||
"I understand, please disable": "我明白,請取消登入認証",
|
||||
Confirm: "確認",
|
||||
Yes: "是",
|
||||
No: "否",
|
||||
Username: "帳號",
|
||||
Password: "密碼",
|
||||
"Remember me": "記住我",
|
||||
Login: "登入",
|
||||
"No Monitors, please": "沒有監測器,請",
|
||||
"add one": "新增",
|
||||
"Notification Type": "通知類型",
|
||||
"Email": "電郵",
|
||||
"Test": "測試",
|
||||
}
|
|
@ -9,23 +9,23 @@
|
|||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
|
||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> New Update
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
|
||||
</a>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<font-awesome-icon icon="tachometer-alt" /> Dashboard
|
||||
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<font-awesome-icon icon="cog" /> Settings
|
||||
<font-awesome-icon icon="cog" /> {{ $t("Settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -48,32 +48,32 @@
|
|||
<footer>
|
||||
<div class="container-fluid">
|
||||
Uptime Kuma -
|
||||
Version: {{ $root.info.version }} -
|
||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
|
||||
{{ $t("Version") }}: {{ $root.info.version }} -
|
||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Mobile Only -->
|
||||
<div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
|
||||
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
|
||||
<nav v-if="$root.isMobile" class="bottom-nav">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||
Dashboard
|
||||
{{ $t("Dashboard") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/list" class="nav-link">
|
||||
<div><font-awesome-icon icon="list" /></div>
|
||||
List
|
||||
{{ $t("List") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/add" class="nav-link">
|
||||
<div><font-awesome-icon icon="plus" /></div>
|
||||
Add
|
||||
{{ $t("Add") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<div><font-awesome-icon icon="cog" /></div>
|
||||
Settings
|
||||
{{ $t("Settings") }}
|
||||
</router-link>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -173,7 +173,7 @@ export default {
|
|||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 160px)
|
||||
min-height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -191,7 +191,7 @@ main {
|
|||
}
|
||||
|
||||
footer {
|
||||
color: #AAA;
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 30px;
|
||||
|
@ -201,11 +201,11 @@ footer {
|
|||
|
||||
.dark {
|
||||
header {
|
||||
background-color: #161B22;
|
||||
border-bottom-color: #161B22 !important;
|
||||
background-color: #161b22;
|
||||
border-bottom-color: #161b22 !important;
|
||||
|
||||
span {
|
||||
color: #F0F6FC;
|
||||
color: #f0f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
20
src/main.js
20
src/main.js
|
@ -1,5 +1,6 @@
|
|||
import "bootstrap";
|
||||
import { createApp, h } from "vue";
|
||||
import { createI18n } from "vue-i18n"
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import Toast from "vue-toastification";
|
||||
import "vue-toastification/dist/index.css";
|
||||
|
@ -22,6 +23,9 @@ import List from "./pages/List.vue";
|
|||
|
||||
import { appName } from "./util.ts";
|
||||
|
||||
import en from "./languages/en";
|
||||
import zhHK from "./languages/zh-HK";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
|
@ -83,6 +87,19 @@ const router = createRouter({
|
|||
routes,
|
||||
})
|
||||
|
||||
const languageList = {
|
||||
en,
|
||||
"zh-HK": zhHK,
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: localStorage.locale || "en",
|
||||
fallbackLocale: "en",
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true,
|
||||
messages: languageList
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
mixins: [
|
||||
socket,
|
||||
|
@ -98,7 +115,8 @@ const app = createApp({
|
|||
render: () => h(App),
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
|
||||
const options = {
|
||||
position: "bottom-right",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="row">
|
||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||
</div>
|
||||
<MonitorList />
|
||||
</div>
|
||||
|
@ -32,6 +32,6 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.container-fluid {
|
||||
width: 98%
|
||||
width: 98%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,63 +2,51 @@
|
|||
<transition name="slide-fade" appear>
|
||||
<div v-if="$route.name === 'DashboardHome'">
|
||||
<h1 class="mb-3">
|
||||
Quick Stats
|
||||
{{ $t("Quick Stats") }}
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Up</h3>
|
||||
<h3>{{ $t("Up") }}</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Down</h3>
|
||||
<h3>{{ $t("Down") }}</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Unknown</h3>
|
||||
<h3>{{ $t("Unknown") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Pause</h3>
|
||||
<h3>{{ $t("Pause") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="false" class="row">
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(30-day)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box" style="margin-top: 25px;overflow-x: scroll">
|
||||
<div class="shadow-box table-shadow-box" style="overflow-x: scroll;">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
<th>{{ $t("Name") }}</th>
|
||||
<th>{{ $t("Status") }}</th>
|
||||
<th>{{ $t("DateTime") }}</th>
|
||||
<th>{{ $t("Message") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
||||
<td>{{ beat.name }}</td>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
<td>{{ beat.msg }}</td>
|
||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||
<td class="border-0">{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="4">
|
||||
No important events
|
||||
{{ $t("No important events") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -182,6 +170,7 @@ export default {
|
|||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
table {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<div v-if="monitor">
|
||||
<h1> {{ monitor.name }}</h1>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
||||
|
@ -14,16 +14,16 @@
|
|||
|
||||
<div class="functions">
|
||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||
<font-awesome-icon icon="pause" /> Pause
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> Resume
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> Edit
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> Delete
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -31,10 +31,10 @@
|
|||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<HeartbeatBar :monitor-id="monitor.id" />
|
||||
<span class="word">Check every {{ monitor.interval }} seconds.</span>
|
||||
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>{{ pingTitle }}</h4>
|
||||
<p>(Current)</p>
|
||||
<p>({{ $t("Current") }})</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
|
||||
<CountUp :value="ping" />
|
||||
|
@ -51,26 +51,26 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Avg. {{ pingTitle }}</h4>
|
||||
<p>(24-hour)</p>
|
||||
<h4>{{ $t("Avg.") }}{{ pingTitle }}</h4>
|
||||
<p>(24{{ $t("-hour") }})</p>
|
||||
<span class="num"><CountUp :value="avgPing" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(24-hour)</p>
|
||||
<h4>{{ $t("Uptime") }}</h4>
|
||||
<p>(24{{ $t("-hour") }})</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(30-day)</p>
|
||||
<h4>{{ $t("Uptime") }}</h4>
|
||||
<p>(30{{ $t("-day") }})</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||
</div>
|
||||
|
||||
<div v-if="certInfo" class="col">
|
||||
<h4>Cert Exp.</h4>
|
||||
<h4>{{ $t("Cert Exp.") }}</h4>
|
||||
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} {{ $t("days") }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -128,25 +128,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="shadow-box table-shadow-box">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
<th>{{ $t("Status") }}</th>
|
||||
<th>{{ $t("DateTime") }}</th>
|
||||
<th>{{ $t("Message") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}" style="padding: 10px;">
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
<td>{{ beat.msg }}</td>
|
||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||
<td class="border-0">{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="3">
|
||||
No important events
|
||||
{{ $t("No important events") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -209,10 +209,9 @@ export default {
|
|||
|
||||
pingTitle() {
|
||||
if (this.monitor.type === "http") {
|
||||
return "Response"
|
||||
return this.$t("Response");
|
||||
}
|
||||
|
||||
return "Ping"
|
||||
return this.$t("Ping");
|
||||
},
|
||||
|
||||
monitor() {
|
||||
|
@ -385,7 +384,7 @@ export default {
|
|||
}
|
||||
|
||||
.word {
|
||||
color: #AAA;
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
@ -399,7 +398,7 @@ table {
|
|||
|
||||
.stats p {
|
||||
font-size: 13px;
|
||||
color: #AAA;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.stats {
|
||||
|
@ -414,7 +413,7 @@ table {
|
|||
color: black;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.dark {
|
||||
.keyword {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">Monitor Type</label>
|
||||
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
|
@ -21,7 +21,7 @@
|
|||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - Keyword
|
||||
HTTP(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="dns">
|
||||
DNS
|
||||
|
@ -30,17 +30,17 @@
|
|||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'keyword' " class="my-3">
|
||||
<label for="keyword" class="form-label">Keyword</label>
|
||||
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
Search keyword in plain html or JSON response and it is case-sensitive
|
||||
|
@ -48,12 +48,12 @@
|
|||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' " class="my-3">
|
||||
<label for="port" class="form-label">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">
|
||||
</div>
|
||||
|
||||
|
@ -93,47 +93,47 @@
|
|||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="maxRetries" class="form-label">Retries</label>
|
||||
<label for="maxRetries" class="form-label">{{ $t("Retries") }}</label>
|
||||
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum retries before the service is marked as down and a notification is sent
|
||||
{{ $t("retriesDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
Ignore TLS/SSL error for HTTPS websites
|
||||
{{ $t("ignoreTLSError") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
Upside Down Mode
|
||||
{{ $t("Upside Down Mode") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||
{{ $t("upsideDownModeDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="maxRedirects" class="form-label">Max. Redirects</label>
|
||||
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum number of redirects to follow. Set to 0 to disable redirects.
|
||||
{{ $t("maxRedirectDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
|
||||
<label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="acceptedStatusCodes"
|
||||
|
@ -150,21 +150,21 @@
|
|||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
Select status codes which are considered as a successful response.
|
||||
{{ $t("acceptedStatusCodesDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2 class="mb-2">Notifications</h2>
|
||||
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
|
||||
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
|
||||
|
@ -172,12 +172,12 @@
|
|||
|
||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||
{{ notification.name }}
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -217,7 +217,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
pageName() {
|
||||
return (this.isAdd) ? "Add New Monitor" : "Edit"
|
||||
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
|
||||
},
|
||||
isAdd() {
|
||||
return this.$route.path === "/add";
|
||||
|
|
|
@ -2,53 +2,63 @@
|
|||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 v-show="show" class="mb-3">
|
||||
Settings
|
||||
{{ $t("Settings") }}
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
<h2 class="mb-2">{{ $t("Appearance") }}</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="language" class="form-label">{{ $t("Language") }}</label>
|
||||
<select id="language" v-model="$i18n.locale" class="form-select">
|
||||
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
|
||||
{{ $i18n.messages[lang].languageName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
|
||||
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||
<label class="btn btn-outline-primary" for="btncheck1">{{ $t("Light") }}</label>
|
||||
|
||||
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||
<label class="btn btn-outline-primary" for="btncheck2">{{ $t("Dark") }}</label>
|
||||
|
||||
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||
<label class="btn btn-outline-primary" for="btncheck3">{{ $t("Auto") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
|
||||
<label class="btn btn-outline-primary" for="btncheck4">{{ $t("Normal") }}</label>
|
||||
|
||||
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
|
||||
<label class="btn btn-outline-primary" for="btncheck5">{{ $t("Bottom") }}</label>
|
||||
|
||||
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
|
||||
<label class="btn btn-outline-primary" for="btncheck6">{{ $t("None") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">{{ $t("General") }}</h2>
|
||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">Theme</label>
|
||||
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||
<label class="btn btn-outline-primary" for="btncheck1">Light</label>
|
||||
|
||||
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||
<label class="btn btn-outline-primary" for="btncheck2">Dark</label>
|
||||
|
||||
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||
<label class="btn btn-outline-primary" for="btncheck3">Auto</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Theme - Heartbeat Bar</label>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
|
||||
<label class="btn btn-outline-primary" for="btncheck4">Normal</label>
|
||||
|
||||
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
|
||||
<label class="btn btn-outline-primary" for="btncheck5">Bottom</label>
|
||||
|
||||
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
|
||||
<label class="btn btn-outline-primary" for="btncheck6">None</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">Timezone</label>
|
||||
<label for="timezone" class="form-label">{{ $t("Timezone") }}</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
Auto: {{ guessTimezone }}
|
||||
{{ $t("Auto") }}: {{ guessTimezone }}
|
||||
</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||
{{ timezone.name }}
|
||||
|
@ -57,65 +67,65 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search Engine Visibility</label>
|
||||
<label class="form-label">{{ $t("Search Engine Visibility") }}</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
|
||||
<label class="form-check-label" for="searchEngineIndexYes">
|
||||
Allow indexing
|
||||
{{ $t("Allow indexing") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
|
||||
<label class="form-check-label" for="searchEngineIndexNo">
|
||||
Discourage search engines from indexing site
|
||||
{{ $t("Discourage search engines from indexing site") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Save
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template v-if="loaded">
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2 class="mt-5 mb-2">Change Password</h2>
|
||||
<h2 class="mt-5 mb-2">{{ $t("Change Password") }}</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<label for="current-password" class="form-label">{{ $t("Current Password") }}</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<label for="new-password" class="form-label">{{ $t("New Password") }}</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<label for="repeat-new-password" class="form-label">{{ $t("Repeat New Password") }}</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
{{ $t("passwordNotMatchMsg") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
{{ $t("Update Password") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -123,23 +133,23 @@
|
|||
<div class="notification-list col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<h2>{{ $t("Notifications") }}</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
Please assign a notification to monitor(s) to get it to work.
|
||||
{{ $t("notificationDescription") }}
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -147,10 +157,18 @@
|
|||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||
<template v-if="$i18n.locale === 'en' ">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</template>
|
||||
|
||||
<template v-if="$i18n.locale === 'zh-HK' ">
|
||||
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
||||
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
||||
<p>請小心使用。</p>
|
||||
</template>
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -195,6 +213,10 @@ export default {
|
|||
"password.repeatNewPassword"() {
|
||||
this.invalidPassword = false;
|
||||
},
|
||||
|
||||
"$i18n.locale"() {
|
||||
localStorage.locale = this.$i18n.locale;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
24
src/util.ts
24
src/util.ts
|
@ -1,4 +1,3 @@
|
|||
// @ts-nocheck
|
||||
// Common Util for frontend and backend
|
||||
// Backend uses the compiled file util.js
|
||||
// Frontend uses util.ts
|
||||
|
@ -13,7 +12,7 @@ export const DOWN = 0;
|
|||
export const UP = 1;
|
||||
export const PENDING = 2;
|
||||
|
||||
export function flipStatus(s) {
|
||||
export function flipStatus(s: number) {
|
||||
if (s === UP) {
|
||||
return DOWN;
|
||||
}
|
||||
|
@ -25,7 +24,7 @@ export function flipStatus(s) {
|
|||
return s;
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
export function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
@ -33,8 +32,8 @@ export function sleep(ms) {
|
|||
* PHP's ucfirst
|
||||
* @param str
|
||||
*/
|
||||
export function ucfirst(str) {
|
||||
if (! str) {
|
||||
export function ucfirst(str: string) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
|
@ -42,12 +41,15 @@ export function ucfirst(str) {
|
|||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
|
||||
export function debug(msg) {
|
||||
export function debug(msg: any) {
|
||||
if (isDev) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
declare global { interface String { replaceAll(str: string, newStr: string): string; } }
|
||||
|
||||
export function polyfill() {
|
||||
/**
|
||||
* String.prototype.replaceAll() polyfill
|
||||
|
@ -56,7 +58,7 @@ export function polyfill() {
|
|||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function (str, newStr) {
|
||||
String.prototype.replaceAll = function (str: string, newStr: string) {
|
||||
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||
|
@ -71,11 +73,13 @@ export function polyfill() {
|
|||
}
|
||||
|
||||
export class TimeLogger {
|
||||
startTime: number;
|
||||
|
||||
constructor() {
|
||||
this.startTime = dayjs().valueOf();
|
||||
}
|
||||
|
||||
print(name) {
|
||||
print(name: string) {
|
||||
if (isDev) {
|
||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
|
||||
}
|
||||
|
@ -85,7 +89,7 @@ export class TimeLogger {
|
|||
/**
|
||||
* Returns a random number between min (inclusive) and max (exclusive)
|
||||
*/
|
||||
export function getRandomArbitrary(min, max) {
|
||||
export function getRandomArbitrary(min: number, max: number) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
|
@ -98,7 +102,7 @@ export function getRandomArbitrary(min, max) {
|
|||
* lower than max if max isn't an integer).
|
||||
* Using Math.round() will give you a non-uniform distribution!
|
||||
*/
|
||||
export function getRandomInt(min, max) {
|
||||
export function getRandomInt(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es2020"
|
||||
"es2020",
|
||||
"DOM",
|
||||
],
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": false,
|
||||
"files.insertFinalNewline": true
|
||||
"strict": true
|
||||
},
|
||||
"files": [
|
||||
"./server/util.ts"
|
||||
"./src/util.ts"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue