mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-17 00:46:56 +08:00
Compare commits
147 Commits
1.15.0
...
1.16.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
398219f847 | ||
|
7a50f0e3f3 | ||
|
4178b003a2 | ||
|
8ede6d888f | ||
|
cec0521834 | ||
|
73b603dd10 | ||
|
92a43e1f30 | ||
|
0cf395dfc3 | ||
|
749ca6f4a8 | ||
|
66971deaf4 | ||
|
8077744c60 | ||
|
c5faf709b8 | ||
|
7da9f139c1 | ||
|
7acbfd2474 | ||
|
9f493bbec7 | ||
|
5bf58cc6c4 | ||
|
d344914ca0 | ||
|
b680371746 | ||
|
e488e2dc0a | ||
|
4e3258579d | ||
|
aa8ea6d398 | ||
|
8b0813ceff | ||
|
91178ce6a5 | ||
|
429ad384d0 | ||
|
4b9dc2890d | ||
|
f9004bcbed | ||
|
bc174c3325 | ||
|
4c2753af46 | ||
|
c6ba5b621c | ||
|
96536ae391 | ||
|
ba46544772 | ||
|
5c852db1cf | ||
|
069d3765f0 | ||
|
15820c6937 | ||
|
000cbeb0ce | ||
|
e118d59ac8 | ||
|
39aa0a7f07 | ||
|
a12dffd1bc | ||
|
410805052e | ||
|
02a8147f22 | ||
|
d962ab7a1c | ||
|
63c8d24d6f | ||
|
254a6bfd36 | ||
|
29f3cbe8c6 | ||
|
53b98ad3e4 | ||
|
dbd7c087e0 | ||
|
272956025c | ||
|
db50ba91cc | ||
|
42ea3fb412 | ||
|
9f8b3151d8 | ||
|
73e38a13d2 | ||
|
369477b4b9 | ||
|
2347a01f7c | ||
|
c114c053d6 | ||
|
ae2c49a729 | ||
|
b9e72b9645 | ||
|
5a069b278d | ||
|
65ea2e6aeb | ||
|
e82fc1df61 | ||
|
7dd5f5ea0d | ||
|
45da7c5431 | ||
|
26230a3d3a | ||
|
82aa52b330 | ||
|
fa7d15cf64 | ||
|
d7f16908d8 | ||
|
bddd5de22b | ||
|
6333231f1b | ||
|
60538036c6 | ||
|
0ba5d031d0 | ||
|
66e4c89897 | ||
|
d210548ae8 | ||
|
023db1450d | ||
|
824c16a07c | ||
|
09fdef9bdc | ||
|
7078b06272 | ||
|
d3bd2976c5 | ||
|
db646aa40b | ||
|
b50f1bb7e8 | ||
|
a3baa3c149 | ||
|
2adb142ae2 | ||
|
752415dae6 | ||
|
1687de163c | ||
|
245b13d3c8 | ||
|
d6c3fdb6fb | ||
|
372bf57e9f | ||
|
03e6f0a6c8 | ||
|
dcec53a755 | ||
|
ce17ed163e | ||
|
3019d5dd64 | ||
|
dcdbb7be8b | ||
|
b874ea8b28 | ||
|
1e595eaa76 | ||
|
5fbfacf5ce | ||
|
d39dc94496 | ||
|
94ada36dfa | ||
|
4114f43b48 | ||
|
4c8da89c36 | ||
|
db3ef3805b | ||
|
b641c8a878 | ||
|
9130b3762c | ||
|
587faecf87 | ||
|
46da5e51be | ||
|
1eecdec2d9 | ||
|
64a33d7455 | ||
|
09e61d9d63 | ||
|
9996ba1636 | ||
|
c2f6c5b42e | ||
|
0083485d4c | ||
|
4ddbf71920 | ||
|
068b920553 | ||
|
3d04befc1f | ||
|
6d22ebedca | ||
|
03b2d8d521 | ||
|
e103ac8335 | ||
|
45f44b183d | ||
|
5a209c74e1 | ||
|
60c63cc18e | ||
|
6fb66728e6 | ||
|
288ed1e3ca | ||
|
0765f05090 | ||
|
2638d68c97 | ||
|
1b1e0f6dd9 | ||
|
0961c6d9b3 | ||
|
ce7d8c38c5 | ||
|
454c1687cf | ||
|
28be32fc68 | ||
|
dd3992063e | ||
|
0313acd4c5 | ||
|
cd19b9fc49 | ||
|
c57b2c4d28 | ||
|
3dda5938f2 | ||
|
f00ec4dfef | ||
|
43f8fc701c | ||
|
499042504f | ||
|
faf6719e7c | ||
|
a9d264ccfc | ||
|
df8f93f0c2 | ||
|
28c0e16a0c | ||
|
6acc9546a0 | ||
|
f455e3a454 | ||
|
7abbf421d0 | ||
|
3625915a85 | ||
|
d74404e106 | ||
|
1c5bce8afa | ||
|
8b5997691e | ||
|
35360e2069 | ||
|
3d002b3ce9 |
@@ -48,6 +48,7 @@ module.exports = {
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675
|
||||
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||
"vue/multi-word-component-names": "off",
|
||||
"no-multi-spaces": [ "error", {
|
||||
ignoreEOLComments: true,
|
||||
}],
|
||||
|
26
.github/workflows/auto-test.yml
vendored
26
.github/workflows/auto-test.yml
vendored
@@ -11,26 +11,42 @@ on:
|
||||
|
||||
jobs:
|
||||
auto-test:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [14.x, 16.x, 17.x]
|
||||
node: [ 14, 16, 17, 18 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
- run: npm run install-legacy
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
env:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||
check-linters:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
|
@@ -27,24 +27,20 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
|
||||
|
||||
## Can I create a pull request for Uptime Kuma?
|
||||
|
||||
⚠️ 2022-03-02 Update:
|
||||
|
||||
Since I found that merging pull requests is a pretty heavy task for me, I try to rearrange it.
|
||||
(Updated 2022-04-24) Since I don't want to waste your time, be sure to create empty draft pull request, so we can discuss first.
|
||||
|
||||
✅ Accept:
|
||||
- Bug/Security fix
|
||||
- Translations
|
||||
- Adding notification providers
|
||||
|
||||
❌ Avoid:
|
||||
⚠️ Discuss First
|
||||
- Large pull requests
|
||||
- New big features
|
||||
|
||||
My long story here: https://www.reddit.com/r/UptimeKuma/comments/t1t6or/comment/hynyijx/
|
||||
- New features
|
||||
|
||||
### Recommended Pull Request Guideline
|
||||
|
||||
Before deep into coding, disscussion first is preferred. Creating an empty pull request for disscussion would be recommended.
|
||||
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||
|
||||
1. Fork the project
|
||||
1. Clone your fork repo to local
|
||||
@@ -79,7 +75,7 @@ I personally do not like something need to learn so much and need to config so m
|
||||
- 4 spaces indentation
|
||||
- Follow `.editorconfig`
|
||||
- Follow ESLint
|
||||
- Methods and funtions should be documented with JSDoc
|
||||
- Methods and functions should be documented with JSDoc
|
||||
|
||||
## Name convention
|
||||
|
||||
@@ -90,9 +86,10 @@ I personally do not like something need to learn so much and need to config so m
|
||||
## Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- NPM >= 8.5
|
||||
- Git
|
||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||
- A SQLite tool (SQLite Expert Personal is suggested)
|
||||
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||
|
||||
## Install dependencies
|
||||
|
||||
@@ -100,39 +97,45 @@ I personally do not like something need to learn so much and need to config so m
|
||||
npm ci
|
||||
```
|
||||
|
||||
## How to start the Backend Dev Server
|
||||
## Dev Server
|
||||
|
||||
(2021-09-23 Update)
|
||||
(2022-04-26 Update)
|
||||
|
||||
We can start the frontend dev server and the backend dev server in one command.
|
||||
|
||||
Port `3000` and port `3001` will be used.
|
||||
|
||||
```bash
|
||||
npm run start-server-dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Backend Server
|
||||
|
||||
It binds to `0.0.0.0:3001` by default.
|
||||
|
||||
### Backend Details
|
||||
|
||||
It is mainly a socket.io app + express.js.
|
||||
|
||||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||
express.js is used for:
|
||||
- entry point such as redirecting to a status page or the dashboard
|
||||
- serving the frontend built files (index.html, .js and .css etc.)
|
||||
- serving internal APIs of status page
|
||||
|
||||
|
||||
### Structure in /server/
|
||||
|
||||
- model/ (Object model, auto mapping to the database table name)
|
||||
- modules/ (Modified 3rd-party modules)
|
||||
- notification-providers/ (individual notification logic)
|
||||
- routers/ (Express Routers)
|
||||
- socket-handler (Socket.io Handlers)
|
||||
- server.js (Server main logic)
|
||||
- server.js (Server entry point and main logic)
|
||||
|
||||
## How to start the Frontend Dev Server
|
||||
## Frontend Dev Server
|
||||
|
||||
1. Set the env var `NODE_ENV` to "development".
|
||||
2. Start the frontend dev server by the following command.
|
||||
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
It binds to `0.0.0.0:3000` by default.
|
||||
For production, it is not used. It will be compiled to `dist` directory instead.
|
||||
|
||||
You can use Vue.js devtools Chrome extension for debugging.
|
||||
|
||||
|
@@ -162,7 +162,7 @@ https://www.reddit.com/r/UptimeKuma/
|
||||
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
|
||||
|
||||
### Bug Reports / Feature Requests
|
||||
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||
If you want to report a bug or request a new feature, feel free to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
|
||||
|
||||
### Translations
|
||||
If you want to translate Uptime Kuma into your language, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
|
@@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8 && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||
rm -rf /root/.cache
|
||||
|
@@ -11,7 +11,7 @@ WORKDIR /app
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8 && \
|
||||
pip3 --no-cache-dir install apprise==0.9.8.3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install cloudflared
|
||||
|
@@ -8,7 +8,7 @@ services:
|
||||
image: louislam/uptime-kuma:1
|
||||
container_name: uptime-kuma
|
||||
volumes:
|
||||
- ./uptime-kuma:/app/data
|
||||
- ./uptime-kuma-data:/app/data
|
||||
ports:
|
||||
- 3001:3001
|
||||
- 3001:3001 # <Host Port>:<Container Port>
|
||||
restart: always
|
||||
|
1322
package-lock.json
generated
1322
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0-beta.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -10,18 +10,20 @@
|
||||
"node": "14.* || >=16.*"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install --legacy-peer-deps",
|
||||
"update-legacy": "npm update --legacy-peer-deps",
|
||||
"install-legacy": "npm install",
|
||||
"update-legacy": "npm update",
|
||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
|
||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "vite --host --config ./config/vite.config.js",
|
||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
"build": "vite build --config ./config/vite.config.js",
|
||||
"test": "npm run lint && node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
|
||||
"test-with-build": "npm run build && npm test",
|
||||
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
|
||||
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
|
||||
@@ -37,7 +39,7 @@
|
||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.15.0 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.15.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@@ -62,10 +64,11 @@
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@louislam/sqlite3": "~15.0.3",
|
||||
"@louislam/sqlite3": "~15.0.6",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.26.1",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "5.1.3",
|
||||
"bree": "~7.1.5",
|
||||
@@ -73,6 +76,7 @@
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"check-password-strength": "^2.0.5",
|
||||
"chroma-js": "^2.1.2",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.8",
|
||||
@@ -122,7 +126,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/eslint-parser": "~7.17.0",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~1.6.4",
|
||||
@@ -130,20 +134,22 @@
|
||||
"@vue/compiler-sfc": "~3.2.31",
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"concurrently": "^7.1.0",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.1",
|
||||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"eslint": "~8.14.0",
|
||||
"eslint-plugin-vue": "~8.7.1",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.3",
|
||||
"npm-check-updates": "^12.5.5",
|
||||
"npm-check-updates": "^12.5.9",
|
||||
"postcss-html": "^1.3.1",
|
||||
"puppeteer": "~13.1.3",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.2.0",
|
||||
"stylelint-config-standard": "~24.0.0",
|
||||
"stylelint": "~14.7.1",
|
||||
"stylelint-config-standard": "~25.0.0",
|
||||
"typescript": "~4.4.4",
|
||||
"vite": "~2.6.14"
|
||||
"vite": "~2.6.14",
|
||||
"wait-on": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -2,6 +2,11 @@ const { R } = require("redbean-node");
|
||||
|
||||
class TwoFA {
|
||||
|
||||
/**
|
||||
* Disable 2FA for specified user
|
||||
* @param {number} userID ID of user to disable
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async disable2FA(userID) {
|
||||
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [
|
||||
userID,
|
||||
|
@@ -5,10 +5,10 @@ const { setting } = require("./util-server");
|
||||
const { loginRateLimiter } = require("./rate-limiter");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param username : string
|
||||
* @param password : string
|
||||
* @returns {Promise<Bean|null>}
|
||||
* Login to web app
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<(Bean|null)>}
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
@@ -34,11 +34,17 @@ exports.login = async function (username, password) {
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that checks if a user is logged in.
|
||||
* @param {string} username The username of the user to check for.
|
||||
* @param {function} callback The callback to call when done, with an error and result parameter.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Callback for myAuthorizer
|
||||
* @callback myAuthorizerCB
|
||||
* @param {any} err Any error encountered
|
||||
* @param {boolean} authorized Is the client authorized?
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {myAuthorizerCB} callback
|
||||
*/
|
||||
function myAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
|
@@ -7,6 +7,7 @@ exports.latestVersion = null;
|
||||
|
||||
let interval;
|
||||
|
||||
/** Start 48 hour check interval */
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
try {
|
||||
@@ -42,6 +43,11 @@ exports.startInterval = () => {
|
||||
interval = setInterval(check, 3600 * 1000 * 48);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable the check update feature
|
||||
* @param {boolean} value Should the check update feature be enabled?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.enableCheckUpdate = async (value) => {
|
||||
await setSetting("checkUpdate", value);
|
||||
|
||||
|
@@ -9,10 +9,9 @@ const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
|
||||
/**
|
||||
* Send a list of notifications to the user.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Send list of notification providers to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
*/
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -35,8 +34,11 @@ async function sendNotificationList(socket) {
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -62,11 +64,12 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||
}
|
||||
|
||||
/**
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param socket
|
||||
* @param monitorID
|
||||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param overwrite Overwrite client-side's heartbeat list
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -91,9 +94,8 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers proxy list
|
||||
*
|
||||
* @param socket
|
||||
* Emit proxy list to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @return {Promise<Bean[]>}
|
||||
*/
|
||||
async function sendProxyList(socket) {
|
||||
@@ -109,9 +111,8 @@ async function sendProxyList(socket) {
|
||||
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendInfo(socket) {
|
||||
socket.emit("info", {
|
||||
|
@@ -1,7 +1,20 @@
|
||||
const args = require("args-parser")(process.argv);
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
naColor: "#999",
|
||||
defaultUpColor: "#66c20a",
|
||||
defaultDownColor: "#c2290a",
|
||||
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||
defaultStyle: "flat",
|
||||
defaultPingValueSuffix: "ms",
|
||||
defaultPingLabelSuffix: "h",
|
||||
defaultUptimeValueSuffix: "%",
|
||||
defaultUptimeLabelSuffix: "h",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
demoMode
|
||||
demoMode,
|
||||
badgeConstants,
|
||||
};
|
||||
|
@@ -58,7 +58,7 @@ class Database {
|
||||
"patch-monitor-expiry-notification.sql": true,
|
||||
"patch-status-page-footer-css.sql": true,
|
||||
"patch-added-mqtt-monitor.sql": true,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The final version should be 10 after merged tag feature
|
||||
@@ -68,6 +68,10 @@ class Database {
|
||||
|
||||
static noReject = true;
|
||||
|
||||
/**
|
||||
* Initialize the database
|
||||
* @param {Object} args Arguments to initialize DB with
|
||||
*/
|
||||
static init(args) {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
@@ -85,6 +89,15 @@ class Database {
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* started in test mode?
|
||||
* @param {boolean} [autoloadModels=true] Should models be
|
||||
* automatically loaded?
|
||||
* @param {boolean} [noLog=false] Should logs not be output?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
@@ -144,6 +157,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/** Patch the database */
|
||||
static async patch() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
|
||||
@@ -189,7 +203,9 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch DB using new process
|
||||
* Call it from patch() only
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2() {
|
||||
@@ -296,9 +312,12 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch database using new patching process
|
||||
* Used it patch2() only
|
||||
* @private
|
||||
* @param sqlFilename
|
||||
* @param databasePatchedFiles
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||
let value = this.patchList[sqlFilename];
|
||||
@@ -333,12 +352,12 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
* @param filename
|
||||
* Load an SQL file and execute it
|
||||
* @param filename Filename of SQL file to import
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async importSQLFile(filename) {
|
||||
|
||||
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
await R.getCell("SELECT 1");
|
||||
|
||||
let text = fs.readFileSync(filename).toString();
|
||||
@@ -366,6 +385,10 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aquire a direct connection to database
|
||||
* @returns {any}
|
||||
*/
|
||||
static getBetterSQLite3Database() {
|
||||
return R.knex.client.acquireConnection();
|
||||
}
|
||||
@@ -401,7 +424,7 @@ class Database {
|
||||
/**
|
||||
* One backup one time in this process.
|
||||
* Reset this.backupPath if you want to backup again
|
||||
* @param version
|
||||
* @param {string} version Version code of backup
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
@@ -423,9 +446,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
/** Restore from most recent backup */
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||
@@ -467,6 +488,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the size of the database */
|
||||
static getSize() {
|
||||
log.debug("db", "Database.getSize()");
|
||||
let stats = fs.statSync(Database.path);
|
||||
@@ -474,6 +496,10 @@ class Database {
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shrink the database
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async shrink() {
|
||||
await R.exec("VACUUM");
|
||||
}
|
||||
|
@@ -8,10 +8,12 @@ const { log } = require("../src/util");
|
||||
let ImageDataURI = (() => {
|
||||
|
||||
/**
|
||||
* @param {string} dataURI - A string that is a valid Data URI.
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Decode the data:image/ URI
|
||||
* @param {string} dataURI data:image/ URI to decode
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64".
|
||||
* The former is the image type, e.g., "png", and the latter is a base64
|
||||
* encoded string of the image's binary data. If it fails to parse, returns
|
||||
* null instead of an object.
|
||||
*/
|
||||
function decode(dataURI) {
|
||||
if (!/data:image\//.test(dataURI)) {
|
||||
@@ -28,11 +30,11 @@ let ImageDataURI = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data - The image data to be encoded.
|
||||
* @param {String} mediaType - The type of the image, e.g., "image/png".
|
||||
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Endcode an image into data:image/ URI
|
||||
* @param {(Buffer|string)} data Data to encode
|
||||
* @param {string} mediaType Media type of data
|
||||
* @returns {(string|null)} A string representing the base64-encoded
|
||||
* version of the given Buffer object or null if an error occurred.
|
||||
*/
|
||||
function encode(data, mediaType) {
|
||||
if (!data || !mediaType) {
|
||||
@@ -48,11 +50,10 @@ let ImageDataURI = (() => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a data URI to a file path.
|
||||
* @param {string} dataURI The Data URI of the image.
|
||||
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Write data URI to file
|
||||
* @param {string} dataURI data:image/ URI
|
||||
* @param {string} [filePath] Path to write file to
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function outputFile(dataURI, filePath) {
|
||||
filePath = filePath || "./";
|
||||
|
@@ -10,6 +10,11 @@ const jobs = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize background jobs
|
||||
* @param {Object} args Arguments to pass to workers
|
||||
* @returns {Bree}
|
||||
*/
|
||||
const initBackgroundJobs = function (args) {
|
||||
bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
|
@@ -2,12 +2,22 @@ const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Send message to parent process for logging
|
||||
* since worker_thread does not have access to stdout, this is used
|
||||
* instead of console.log()
|
||||
* @param {any} any The message to log
|
||||
*/
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exit the worker process
|
||||
* @param {number} error The status code to exit
|
||||
*/
|
||||
const exit = function (error) {
|
||||
if (error && error !== 0) {
|
||||
process.exit(error);
|
||||
@@ -20,6 +30,7 @@ const exit = function (error) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Connects to the database */
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
|
@@ -3,6 +3,12 @@ const { R } = require("redbean-node");
|
||||
|
||||
class Group extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
@@ -19,6 +25,10 @@ class Group extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
* @returns {Bean[]}
|
||||
*/
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
SELECT monitor.* FROM monitor, monitor_group
|
||||
|
@@ -13,6 +13,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
*/
|
||||
class Heartbeat extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
status: this.status,
|
||||
@@ -22,6 +27,10 @@ class Heartbeat extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
monitorID: this.monitor_id,
|
||||
|
@@ -2,6 +2,11 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Incident extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
|
@@ -7,7 +7,7 @@ dayjs.extend(timezone);
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog, mqttAsync } = require("../util-server");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mqttAsync } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
@@ -15,6 +15,7 @@ const { Proxy } = require("../proxy");
|
||||
const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -27,6 +28,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
let obj = {
|
||||
@@ -41,6 +43,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON(includeSensitiveData = true) {
|
||||
|
||||
@@ -101,6 +104,10 @@ class Monitor extends BeanModel {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||
}
|
||||
@@ -114,6 +121,10 @@ class Monitor extends BeanModel {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the TLS expiry notification enabled?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEnabledExpiryNotification() {
|
||||
return Boolean(this.expiryNotification);
|
||||
}
|
||||
@@ -134,10 +145,18 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accepted status codes
|
||||
* @returns {Object}
|
||||
*/
|
||||
getAcceptedStatuscodes() {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
*/
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
@@ -163,7 +182,7 @@ class Monitor extends BeanModel {
|
||||
// undefined if not https
|
||||
let tlsInfo = undefined;
|
||||
|
||||
if (!previousBeat) {
|
||||
if (!previousBeat || this.type === "push") {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
this.id,
|
||||
]);
|
||||
@@ -358,9 +377,6 @@ class Monitor extends BeanModel {
|
||||
log.debug("monitor", "heartbeatCount" + heartbeatCount + " " + time);
|
||||
|
||||
if (heartbeatCount <= 0) {
|
||||
// Fix #922, since previous heartbeat could be inserted by api, it should get from database
|
||||
previousBeat = await Monitor.getPreviousHeartbeat(this.id);
|
||||
|
||||
throw new Error("No heartbeat in the time window");
|
||||
} else {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
@@ -497,12 +513,13 @@ class Monitor extends BeanModel {
|
||||
|
||||
};
|
||||
|
||||
/** Get a heartbeat and handle errors */
|
||||
const safeBeat = async () => {
|
||||
try {
|
||||
await beat();
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
errorLog(e, false);
|
||||
UptimeKumaServer.errorLog(e, false);
|
||||
log.error("monitor", "Please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
if (! this.isStop) {
|
||||
@@ -522,6 +539,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop monitor */
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
@@ -529,6 +547,10 @@ class Monitor extends BeanModel {
|
||||
this.prometheus().remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new prometheus instance
|
||||
* @returns {Prometheus}
|
||||
*/
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
}
|
||||
@@ -537,7 +559,7 @@ class Monitor extends BeanModel {
|
||||
* Helper Method:
|
||||
* returns URL object for further usage
|
||||
* returns null if url is invalid
|
||||
* @returns {null|URL}
|
||||
* @returns {(null|URL)}
|
||||
*/
|
||||
getUrl() {
|
||||
try {
|
||||
@@ -550,7 +572,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
* @returns {Promise<object>}
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async updateTlsInfo(checkCertificateResult) {
|
||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
@@ -592,6 +614,12 @@ class Monitor extends BeanModel {
|
||||
return checkCertificateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send statistics to clients
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendStats(io, monitorID, userID) {
|
||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||
|
||||
@@ -606,8 +634,8 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param duration : int Hours
|
||||
* Send the average ping to user
|
||||
* @param {number} duration Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -627,6 +655,12 @@ class Monitor extends BeanModel {
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send certificate information to client
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendCertInfo(io, monitorID, userID) {
|
||||
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
monitorID,
|
||||
@@ -640,7 +674,8 @@ class Monitor extends BeanModel {
|
||||
* Uptime with calculation
|
||||
* Calculation based on:
|
||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||
* @param duration : int Hours
|
||||
* @param {number} duration Hours
|
||||
* @param {number} monitorID ID of monitor to calculate
|
||||
*/
|
||||
static async calcUptime(duration, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -694,7 +729,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
} else {
|
||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [monitorID]));
|
||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||
|
||||
if (status === UP) {
|
||||
uptime = 1;
|
||||
@@ -706,13 +741,23 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Send Uptime
|
||||
* @param duration : int Hours
|
||||
* @param {number} duration Hours
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Has status of monitor changed since last beat?
|
||||
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||
* @param {const} previousBeatStatus Status of the previous beat
|
||||
* @param {const} currentBeatStatus Status of the current beat
|
||||
* @returns {boolean} True if is an important beat else false
|
||||
*/
|
||||
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
@@ -731,6 +776,12 @@ class Monitor extends BeanModel {
|
||||
return isImportant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification about a monitor
|
||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||
* @param {Bean} bean Status information about monitor
|
||||
*/
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
const notificationList = await Monitor.getNotificationList(monitor);
|
||||
@@ -755,6 +806,11 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of notification providers for a given monitor
|
||||
* @param {Monitor} monitor Monitor to get notification providers for
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
static async getNotificationList(monitor) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
monitor.id,
|
||||
@@ -762,6 +818,10 @@ class Monitor extends BeanModel {
|
||||
return notificationList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about a certificate
|
||||
* @param {Object} tlsInfoObject Information about certificate
|
||||
*/
|
||||
async sendCertNotification(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
const notificationList = await Monitor.getNotificationList(this);
|
||||
@@ -773,6 +833,14 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a certificate notification when certificate expires in less
|
||||
* than target days
|
||||
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||
* @param {number} targetDays Number of days to alert after
|
||||
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||
|
||||
if (daysRemaining > targetDays) {
|
||||
@@ -820,6 +888,11 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of the previous heartbeat
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getPreviousHeartbeat(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
|
@@ -1,6 +1,10 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Proxy extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this._id,
|
||||
|
@@ -6,6 +6,7 @@ class StatusPage extends BeanModel {
|
||||
static domainMappingList = { };
|
||||
|
||||
/**
|
||||
* Loads domain mapping from DB
|
||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -17,6 +18,12 @@ class StatusPage extends BeanModel {
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send status page list to client
|
||||
* @param {Server} io io Socket server instance
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
*/
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
|
||||
@@ -30,6 +37,11 @@ class StatusPage extends BeanModel {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update list of domain names
|
||||
* @param {string[]} domainNameList
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateDomainNameList(domainNameList) {
|
||||
|
||||
if (!Array.isArray(domainNameList)) {
|
||||
@@ -69,6 +81,10 @@ class StatusPage extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of domain names
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getDomainNameList() {
|
||||
let domainList = [];
|
||||
for (let domain in StatusPage.domainMappingList) {
|
||||
@@ -81,6 +97,10 @@ class StatusPage extends BeanModel {
|
||||
return domainList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -98,6 +118,11 @@ class StatusPage extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
slug: this.slug,
|
||||
@@ -113,12 +138,20 @@ class StatusPage extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert slug to status page ID
|
||||
* @param {string} slug
|
||||
*/
|
||||
static async slugToID(slug) {
|
||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to the icon for the page
|
||||
* @returns {string}
|
||||
*/
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
return "/icon.svg";
|
||||
|
@@ -1,6 +1,11 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class Tag extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this._id,
|
||||
|
@@ -3,12 +3,11 @@ const passwordHash = require("../password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class User extends BeanModel {
|
||||
|
||||
/**
|
||||
*
|
||||
* Reset user password
|
||||
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||
* @param userID
|
||||
* @param newPassword
|
||||
* @param {number} userID ID of user to update
|
||||
* @param {string} newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPassword(userID, newPassword) {
|
||||
@@ -19,8 +18,8 @@ class User extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param newPassword
|
||||
* Reset this users password
|
||||
* @param {string} newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
|
@@ -13,27 +13,49 @@ let t = {
|
||||
|
||||
let instances = [];
|
||||
|
||||
/**
|
||||
* Does a === b
|
||||
* @param {any} a
|
||||
* @returns {function(any): boolean}
|
||||
*/
|
||||
let matches = function (a) {
|
||||
return function (b) {
|
||||
return a === b;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Does a!==b
|
||||
* @param {any} a
|
||||
* @returns {function(any): boolean}
|
||||
*/
|
||||
let doesntMatch = function (a) {
|
||||
return function (b) {
|
||||
return !matches(a)(b);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get log duration
|
||||
* @param {number} d Time in ms
|
||||
* @param {string} prefix Prefix for log
|
||||
* @returns {string} Coloured log string
|
||||
*/
|
||||
let logDuration = function (d, prefix) {
|
||||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get safe headers
|
||||
* @param {Object} res Express response object
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getSafeHeaders(res) {
|
||||
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||
}
|
||||
|
||||
/** Constructor for ApiCache instance */
|
||||
function ApiCache() {
|
||||
let memCache = new MemoryCache();
|
||||
|
||||
@@ -70,10 +92,10 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the `DEBUG` environment variable is set.
|
||||
* @param {string} a - The first argument to log.
|
||||
* @param {string} b - The second argument to log.
|
||||
* @param {string} c - The third argument to log.
|
||||
* @param {string} d - The fourth argument to log, and so on... (optional)
|
||||
* @param {string} a The first argument to log.
|
||||
* @param {string} b The second argument to log.
|
||||
* @param {string} c The third argument to log.
|
||||
* @param {string} d The fourth argument to log, and so on... (optional)
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@@ -90,8 +112,8 @@ function ApiCache() {
|
||||
* Returns true if the given request and response should be logged.
|
||||
* @param {Object} request The HTTP request object.
|
||||
* @param {Object} response The HTTP response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldCacheResponse(request, response, toggle) {
|
||||
let opt = globalOptions;
|
||||
@@ -116,10 +138,9 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key to the index.
|
||||
* @param {string} key The key to add.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Add key to index array
|
||||
* @param {string} key Key to add
|
||||
* @param {Object} req Express request object
|
||||
*/
|
||||
function addIndexEntries(key, req) {
|
||||
let groupName = req.apicacheGroup;
|
||||
@@ -135,8 +156,11 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Returns a new object containing only the whitelisted headers.
|
||||
* @param {Object} headers The original object of header names and values.
|
||||
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
|
||||
* @param {Object} headers The original object of header names and
|
||||
* values.
|
||||
* @param {string[]} globalOptions.headerWhitelist An array of
|
||||
* strings representing the whitelisted header names to keep in the
|
||||
* output object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@@ -152,8 +176,10 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache object
|
||||
* @param {Object} headers The response headers to filter.
|
||||
* @returns {Object} A new object containing only the whitelisted response headers.
|
||||
* @returns {Object} A new object containing only the whitelisted
|
||||
* response headers.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@@ -170,8 +196,9 @@ function ApiCache() {
|
||||
/**
|
||||
* Sets a cache value for the given key.
|
||||
* @param {string} key The cache key to set.
|
||||
* @param {*} value The cache value to set.
|
||||
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
|
||||
* @param {any} value The cache value to set.
|
||||
* @param {number} duration How long in milliseconds the cached
|
||||
* response should be valid for (defaults to 1 hour).
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@@ -199,7 +226,8 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Appends content to the response.
|
||||
* @param {string|Buffer} content The content to append.
|
||||
* @param {Object} res Express response object
|
||||
* @param {(string|Buffer)} content The content to append.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
@@ -229,11 +257,15 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Monkeypatches the response object to add cache control headers and create a cache object.
|
||||
* @param {Object} req - The request object.
|
||||
* @param {Object} res - The response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Monkeypatches the response object to add cache control headers
|
||||
* and create a cache object.
|
||||
* @param {Object} req Express request object
|
||||
* @param {Object} res Express response object
|
||||
* @param {function} next Function to call next
|
||||
* @param {string} key Key to add response as
|
||||
* @param {number} duration Time to cache response for
|
||||
* @param {string} strDuration Duration in string form
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
*/
|
||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||
// monkeypatch res.end to create cache object
|
||||
@@ -302,11 +334,15 @@ function ApiCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {Response} response
|
||||
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Send a cached response to client
|
||||
* @param {Request} request Express request object
|
||||
* @param {Response} response Express response object
|
||||
* @param {object} cacheObject Cache object to send
|
||||
* @param {function(Object, Object):boolean} toggle
|
||||
* @param {function} next Function to call next
|
||||
* @param {number} duration Not used
|
||||
* @returns {boolean|undefined} true if the request should be
|
||||
* cached, false otherwise. If undefined, defaults to true.
|
||||
*/
|
||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||
if (toggle && !toggle(request, response)) {
|
||||
@@ -348,12 +384,19 @@ function ApiCache() {
|
||||
return response.end(data, cacheObject.encoding);
|
||||
}
|
||||
|
||||
/** Sync caching options */
|
||||
function syncOptions() {
|
||||
for (let i in middlewareOptions) {
|
||||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear key from cache
|
||||
* @param {string} target Key to clear
|
||||
* @param {boolean} isAutomatic Is the key being cleared automatically
|
||||
* @returns {number}
|
||||
*/
|
||||
this.clear = function (target, isAutomatic) {
|
||||
let group = index.groups[target];
|
||||
let redis = globalOptions.redisClient;
|
||||
@@ -430,10 +473,11 @@ function ApiCache() {
|
||||
|
||||
/**
|
||||
* Converts a duration string to an integer number of milliseconds.
|
||||
* @param {string} duration - The string to convert.
|
||||
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @param {(string|number)} duration The string to convert.
|
||||
* @param {number} defaultDuration The default duration to return if
|
||||
* can't parse duration
|
||||
* @returns {number} The converted value in milliseconds, or the
|
||||
* defaultDuration if it can't be parsed.
|
||||
*/
|
||||
function parseDuration(duration, defaultDuration) {
|
||||
if (typeof duration === "number") {
|
||||
@@ -457,17 +501,24 @@ function ApiCache() {
|
||||
return defaultDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse duration
|
||||
* @param {(number|string)} duration
|
||||
* @returns {number} Duration parsed to a number
|
||||
*/
|
||||
this.getDuration = function (duration) {
|
||||
return parseDuration(duration, globalOptions.defaultDuration);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
||||
* Return cache performance statistics (hit rate). Suitable for
|
||||
* putting into a route:
|
||||
* <code>
|
||||
* app.get('/api/cache/performance', (req, res) => {
|
||||
* res.json(apicache.getPerformance())
|
||||
* })
|
||||
* </code>
|
||||
* @returns {any[]}
|
||||
*/
|
||||
this.getPerformance = function () {
|
||||
return performanceArray.map(function (p) {
|
||||
@@ -475,6 +526,11 @@ function ApiCache() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get index of a group
|
||||
* @param {string} group
|
||||
* @returns {number}
|
||||
*/
|
||||
this.getIndex = function (group) {
|
||||
if (group) {
|
||||
return index.groups[group];
|
||||
@@ -483,6 +539,14 @@ function ApiCache() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Express middleware
|
||||
* @param {(string|number)} strDuration Duration to cache responses
|
||||
* for.
|
||||
* @param {function(Object, Object):boolean} middlewareToggle
|
||||
* @param {Object} localOptions Options for APICache
|
||||
* @returns
|
||||
*/
|
||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||
let duration = instance.getDuration(strDuration);
|
||||
let opt = {};
|
||||
@@ -506,63 +570,72 @@ function ApiCache() {
|
||||
options(localOptions);
|
||||
|
||||
/**
|
||||
* A Function for non tracking performance
|
||||
*/
|
||||
* A Function for non tracking performance
|
||||
*/
|
||||
function NOOPCachePerformance() {
|
||||
this.report = this.hit = this.miss = function () {}; // noop;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
||||
*/
|
||||
* A function for tracking and reporting hit rate. These
|
||||
* statistics are returned by the getPerformance() call above.
|
||||
*/
|
||||
function CachePerformance() {
|
||||
/**
|
||||
* Tracks the hit rate for the last 100 requests.
|
||||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 100 requests. If there
|
||||
* have been fewer than 100 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 1000 requests.
|
||||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 1000 requests. If there
|
||||
* have been fewer than 1000 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 10000 requests.
|
||||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 10000 requests. If there
|
||||
* have been fewer than 10000 requests, the hit rate just
|
||||
* considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* Tracks the hit rate for the last 100000 requests.
|
||||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
||||
*/
|
||||
* Tracks the hit rate for the last 100000 requests. If
|
||||
* there have been fewer than 100000 requests, the hit rate
|
||||
* just considers the requests that have happened.
|
||||
*/
|
||||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||
|
||||
/**
|
||||
* The number of calls that have passed through the middleware since the server started.
|
||||
*/
|
||||
* The number of calls that have passed through the
|
||||
* middleware since the server started.
|
||||
*/
|
||||
this.callCount = 0;
|
||||
|
||||
/**
|
||||
* The total number of hits since the server started
|
||||
*/
|
||||
* The total number of hits since the server started
|
||||
*/
|
||||
this.hitCount = 0;
|
||||
|
||||
/**
|
||||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
* The key from the last cache hit. This is useful in
|
||||
* identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheHit = null;
|
||||
|
||||
/**
|
||||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
||||
*/
|
||||
* The key from the last cache miss. This is useful in
|
||||
* identifying which route these statistics apply to.
|
||||
*/
|
||||
this.lastCacheMiss = null;
|
||||
|
||||
/**
|
||||
* Return performance statistics
|
||||
*/
|
||||
* Return performance statistics
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.report = function () {
|
||||
return {
|
||||
lastCacheHit: this.lastCacheHit,
|
||||
@@ -579,10 +652,13 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes a cache hit rate from an array of hits and misses.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
||||
*/
|
||||
* Computes a cache hit rate from an array of hits and
|
||||
* misses.
|
||||
* @param {Uint8Array} array An array representing hits and
|
||||
* misses.
|
||||
* @returns {?number} a number between 0 and 1, or null if
|
||||
* the array has no hits or misses
|
||||
*/
|
||||
this.hitRate = function (array) {
|
||||
let hits = 0;
|
||||
let misses = 0;
|
||||
@@ -608,16 +684,17 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Record a hit or miss in the given array. It will be recorded at a position determined
|
||||
* by the current value of the callCount variable.
|
||||
* @param {Uint8Array} array An array representing hits and misses.
|
||||
* @param {boolean} hit true for a hit, false for a miss
|
||||
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
||||
* Each hit or miss is encoded as to bits as follows:
|
||||
* 00 means no hit or miss has been recorded in these bits
|
||||
* 01 encodes a hit
|
||||
* 10 encodes a miss
|
||||
*/
|
||||
* Record a hit or miss in the given array. It will be
|
||||
* recorded at a position determined by the current value of
|
||||
* the callCount variable.
|
||||
* @param {Uint8Array} array An array representing hits and
|
||||
* misses.
|
||||
* @param {boolean} hit true for a hit, false for a miss
|
||||
* Each element in the array is 8 bits, and encodes 4
|
||||
* hit/miss records. Each hit or miss is encoded as to bits
|
||||
* as follows: 00 means no hit or miss has been recorded in
|
||||
* these bits 01 encodes a hit 10 encodes a miss
|
||||
*/
|
||||
this.recordHitInArray = function (array, hit) {
|
||||
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||
@@ -627,9 +704,11 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records the hit or miss in the tracking arrays and increments the call count.
|
||||
* @param {boolean} hit true records a hit, false records a miss
|
||||
*/
|
||||
* Records the hit or miss in the tracking arrays and
|
||||
* increments the call count.
|
||||
* @param {boolean} hit true records a hit, false records a
|
||||
* miss
|
||||
*/
|
||||
this.recordHit = function (hit) {
|
||||
this.recordHitInArray(this.hitsLast100, hit);
|
||||
this.recordHitInArray(this.hitsLast1000, hit);
|
||||
@@ -642,18 +721,18 @@ function ApiCache() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a hit event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache hit
|
||||
*/
|
||||
* Records a hit event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache hit
|
||||
*/
|
||||
this.hit = function (key) {
|
||||
this.recordHit(true);
|
||||
this.lastCacheHit = key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a miss event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache miss
|
||||
*/
|
||||
* Records a miss event, setting lastCacheMiss to the given key
|
||||
* @param {string} key The key that had the cache miss
|
||||
*/
|
||||
this.miss = function (key) {
|
||||
this.recordHit(false);
|
||||
this.lastCacheMiss = key;
|
||||
@@ -664,6 +743,13 @@ function ApiCache() {
|
||||
|
||||
performanceArray.push(perf);
|
||||
|
||||
/**
|
||||
* Cache a request
|
||||
* @param {Object} req Express request object
|
||||
* @param {Object} res Express response object
|
||||
* @param {function} next Function to call next
|
||||
* @returns {any}
|
||||
*/
|
||||
let cache = function (req, res, next) {
|
||||
function bypass() {
|
||||
debug("bypass detected, skipping cache.");
|
||||
@@ -771,6 +857,11 @@ function ApiCache() {
|
||||
return cache;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process options
|
||||
* @param {Object} options
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.options = function (options) {
|
||||
if (options) {
|
||||
Object.assign(globalOptions, options);
|
||||
@@ -791,6 +882,7 @@ function ApiCache() {
|
||||
}
|
||||
};
|
||||
|
||||
/** Reset the index */
|
||||
this.resetIndex = function () {
|
||||
index = {
|
||||
all: [],
|
||||
@@ -798,6 +890,11 @@ function ApiCache() {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new instance of ApiCache
|
||||
* @param {Object} config Config to pass
|
||||
* @returns {ApiCache}
|
||||
*/
|
||||
this.newInstance = function (config) {
|
||||
let instance = new ApiCache();
|
||||
|
||||
@@ -808,6 +905,7 @@ function ApiCache() {
|
||||
return instance;
|
||||
};
|
||||
|
||||
/** Clone this instance */
|
||||
this.clone = function () {
|
||||
return this.newInstance(this.options());
|
||||
};
|
||||
|
@@ -3,6 +3,15 @@ function MemoryCache() {
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key Key to store cache as
|
||||
* @param {any} value Value to store
|
||||
* @param {number} time Time to store for
|
||||
* @param {function(any, string)} timeoutCallback Callback to call in
|
||||
* case of timeout
|
||||
* @returns {Object}
|
||||
*/
|
||||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||
let old = this.cache[key];
|
||||
let instance = this;
|
||||
@@ -22,6 +31,11 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||
return entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a cache entry
|
||||
* @param {string} key Key to delete
|
||||
* @returns {null}
|
||||
*/
|
||||
MemoryCache.prototype.delete = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
@@ -36,18 +50,32 @@ MemoryCache.prototype.delete = function (key) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get value of key
|
||||
* @param {string} key
|
||||
* @returns {Object}
|
||||
*/
|
||||
MemoryCache.prototype.get = function (key) {
|
||||
let entry = this.cache[key];
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get value of cache entry
|
||||
* @param {string} key
|
||||
* @returns {any}
|
||||
*/
|
||||
MemoryCache.prototype.getValue = function (key) {
|
||||
let entry = this.get(key);
|
||||
|
||||
return entry && entry.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* @returns {boolean}
|
||||
*/
|
||||
MemoryCache.prototype.clear = function () {
|
||||
Object.keys(this.cache).forEach(function (key) {
|
||||
this.delete(key);
|
||||
|
@@ -37,6 +37,12 @@ class AliyunSMS extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the SMS notification
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @param {string} msgbody Message template
|
||||
* @returns {boolean} True if successful else false
|
||||
*/
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
PhoneNumbers: notification.phonenumber,
|
||||
@@ -70,7 +76,12 @@ class AliyunSMS extends NotificationProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Aliyun request sign */
|
||||
/**
|
||||
* Aliyun request sign
|
||||
* @param {Object} param Parameters object to sign
|
||||
* @param {string} AccessKeySecret Secret key to sign parameters with
|
||||
* @returns {string}
|
||||
*/
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
let data = [];
|
||||
@@ -82,8 +93,23 @@ class AliyunSMS extends NotificationProvider {
|
||||
param2[key] = param[key];
|
||||
}
|
||||
|
||||
// Escape more characters than encodeURIComponent does.
|
||||
// For generating Aliyun signature, all characters except A-Za-z0-9~-._ are encoded.
|
||||
// See https://help.aliyun.com/document_detail/315526.html
|
||||
// This encoding methods as known as RFC 3986 (https://tools.ietf.org/html/rfc3986)
|
||||
let moreEscapesTable = function (m) {
|
||||
return {
|
||||
"!": "%21",
|
||||
"*": "%2A",
|
||||
"'": "%27",
|
||||
"(": "%28",
|
||||
")": "%29"
|
||||
}[m];
|
||||
};
|
||||
|
||||
for (let key in param2) {
|
||||
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
|
||||
let value = encodeURIComponent(param2[key]).replace(/[!*'()]/g, moreEscapesTable);
|
||||
data.push(`${encodeURIComponent(key)}=${value}`);
|
||||
}
|
||||
|
||||
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
|
||||
@@ -93,6 +119,11 @@ class AliyunSMS extends NotificationProvider {
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
*/
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
|
@@ -6,9 +6,14 @@ class Apprise extends NotificationProvider {
|
||||
name = "apprise";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let s = childProcess.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL ]);
|
||||
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||
if (notification.title) {
|
||||
args.push("-t");
|
||||
args.push(notification.title);
|
||||
}
|
||||
const s = childProcess.spawnSync("apprise", args);
|
||||
|
||||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
|
@@ -44,7 +44,12 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// add additional parameter for better on device styles (iOS 15 optimized)
|
||||
/**
|
||||
* Add additional parameter for better on device styles (iOS 15
|
||||
* optimized)
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string}
|
||||
*/
|
||||
appendAdditionalParameters(postUrl) {
|
||||
// grouping all our notifications
|
||||
postUrl += "?group=" + barkNotificationGroup;
|
||||
@@ -55,7 +60,11 @@ class Bark extends NotificationProvider {
|
||||
return postUrl;
|
||||
}
|
||||
|
||||
// thrown if failed to check result, result code should be in range 2xx
|
||||
/**
|
||||
* Check if result is successful
|
||||
* @param {Object} result Axios response object
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("Bark notification failed with invalid response!");
|
||||
@@ -65,6 +74,13 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {string} title Message title
|
||||
* @param {string} subtitle Message
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(title, subtitle, endpoint) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
|
@@ -37,6 +37,12 @@ class DingDing extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to DingDing
|
||||
* @param {BeanModel} notification
|
||||
* @param {Object} params Parameters of message
|
||||
* @returns {boolean} True if successful else false
|
||||
*/
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
|
||||
@@ -56,7 +62,12 @@ class DingDing extends NotificationProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** DingDing sign */
|
||||
/**
|
||||
* DingDing sign
|
||||
* @param {Date} timestamp Timestamp of message
|
||||
* @param {string} secretKey Secret key to sign data with
|
||||
* @returns {string}
|
||||
*/
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
|
||||
@@ -64,7 +75,13 @@ class DingDing extends NotificationProvider {
|
||||
.digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
*/
|
||||
statusToString(status) {
|
||||
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
|
@@ -22,16 +22,23 @@ class Discord extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
let address;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
address = monitorJSON["url"];
|
||||
break;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
@@ -48,8 +55,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url,
|
||||
name: "Service URL / Address",
|
||||
value: address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
@@ -84,7 +91,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: "Service URL",
|
||||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
|
||||
value: address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
|
@@ -7,17 +7,23 @@ class NotificationProvider {
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} msg General Message
|
||||
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Return Successful Message
|
||||
* Throw Error with fail msg
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
throw new Error("Have to override Notification.send(...)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error
|
||||
* @param {any} error The error to throw
|
||||
* @throws {any} The error specified
|
||||
*/
|
||||
throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
|
@@ -10,6 +10,7 @@ class Slack extends NotificationProvider {
|
||||
/**
|
||||
* Deprecated property notification.slackbutton
|
||||
* Set it as primary base url if this is not yet set.
|
||||
* @param {string} url The primary base URL to use
|
||||
*/
|
||||
static async deprecateURL(url) {
|
||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||
|
@@ -5,6 +5,12 @@ const { DOWN, UP } = require("../../src/util");
|
||||
class Teams extends NotificationProvider {
|
||||
name = "teams";
|
||||
|
||||
/**
|
||||
* Generate the message to send
|
||||
* @param {const} status The status constant
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @returns {string}
|
||||
*/
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
if (status === DOWN) {
|
||||
return `🔴 Application [${monitorName}] went down`;
|
||||
@@ -14,6 +20,11 @@ class Teams extends NotificationProvider {
|
||||
return "Notification";
|
||||
};
|
||||
|
||||
/**
|
||||
* Select theme color to use based on status
|
||||
* @param {const} status The status constant
|
||||
* @returns {string} Selected color in hex RGB format
|
||||
*/
|
||||
_getThemeColor = (status) => {
|
||||
if (status === DOWN) {
|
||||
return "ff0000";
|
||||
@@ -24,6 +35,14 @@ class Teams extends NotificationProvider {
|
||||
return "008cff";
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate payload for notification
|
||||
* @param {const} status The status of the monitor
|
||||
* @param {string} monitorMessage Message to send
|
||||
* @param {string} monitorName Name of monitor affected
|
||||
* @param {string} monitorUrl URL of monitor affected
|
||||
* @returns {Object}
|
||||
*/
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
monitorMessage,
|
||||
@@ -74,10 +93,21 @@ class Teams extends NotificationProvider {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the notification
|
||||
* @param {string} webhookUrl URL to send the request to
|
||||
* @param {Object} payload Payload generated by _notificationPayloadFactory
|
||||
*/
|
||||
_sendNotification = async (webhookUrl, payload) => {
|
||||
await axios.post(webhookUrl, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a general notification
|
||||
* @param {string} webhookUrl URL to send request to
|
||||
* @param {string} msg Message to send
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: msg
|
||||
|
@@ -24,6 +24,12 @@ class WeCom extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the message to send
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {string} msg General message
|
||||
* @returns {Object}
|
||||
*/
|
||||
composeMessage(heartbeatJSON, msg) {
|
||||
let title;
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
|
@@ -38,6 +38,7 @@ class Notification {
|
||||
|
||||
providerList = {};
|
||||
|
||||
/** Initialize the notification providers */
|
||||
static init() {
|
||||
log.info("notification", "Prepare Notification Providers");
|
||||
|
||||
@@ -92,13 +93,13 @@ class Notification {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param notification : BeanModel
|
||||
* @param msg : string General Message
|
||||
* @param monitorJSON : object Monitor details (For Up/Down only)
|
||||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} msg General Message
|
||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* Throw Error with fail msg
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
if (this.providerList[notification.type]) {
|
||||
@@ -108,6 +109,13 @@ class Notification {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a notification
|
||||
* @param {Object} notification Notification to save
|
||||
* @param {?number} notificationID ID of notification to update
|
||||
* @param {number} userID ID of user who adds notification
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
static async save(notification, notificationID, userID) {
|
||||
let bean;
|
||||
|
||||
@@ -138,6 +146,12 @@ class Notification {
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification
|
||||
* @param {number} notificationID ID of notification to delete
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(notificationID, userID) {
|
||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [
|
||||
notificationID,
|
||||
@@ -151,6 +165,10 @@ class Notification {
|
||||
await R.trash(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if apprise exists
|
||||
* @returns {boolean} Does the command apprise exist?
|
||||
*/
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require("command-exists").sync;
|
||||
let exists = commandExistsSync("apprise");
|
||||
@@ -160,11 +178,10 @@ class Notification {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new monitor to the database.
|
||||
* @param {number} userID The ID of the user that owns this monitor.
|
||||
* @param {string} name The name of this monitor.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Apply the notification to every monitor
|
||||
* @param {number} notificationID ID of notification to apply
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
||||
|
@@ -2,10 +2,21 @@ const passwordHashOld = require("password-hash");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const saltRounds = 10;
|
||||
|
||||
/**
|
||||
* Hash a password
|
||||
* @param {string} password
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.generate = function (password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify a password against a hash
|
||||
* @param {string} password
|
||||
* @param {string} hash
|
||||
* @returns {boolean} Does the password match the hash?
|
||||
*/
|
||||
exports.verify = function (password, hash) {
|
||||
if (isSHA1(hash)) {
|
||||
return passwordHashOld.verify(password, hash);
|
||||
@@ -14,10 +25,19 @@ exports.verify = function (password, hash) {
|
||||
return bcrypt.compareSync(password, hash);
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the hash a SHA1 hash
|
||||
* @param {string} hash
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSHA1(hash) {
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the hash need to be rehashed?
|
||||
* @returns {boolean}
|
||||
*/
|
||||
exports.needRehash = function (hash) {
|
||||
return isSHA1(hash);
|
||||
};
|
||||
|
@@ -9,11 +9,10 @@ const util = require("./util-server");
|
||||
module.exports = Ping;
|
||||
|
||||
/**
|
||||
* @param {string} host - The host to ping
|
||||
* @param {object} [options] - Options for the ping command
|
||||
* Constructor for ping class
|
||||
* @param {string} host Host to ping
|
||||
* @param {object} [options] Options for the ping command
|
||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function Ping(host, options) {
|
||||
if (!host) {
|
||||
@@ -82,8 +81,17 @@ function Ping(host, options) {
|
||||
|
||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||
|
||||
// SEND A PING
|
||||
// ===========
|
||||
/**
|
||||
* Callback for send
|
||||
* @callback pingCB
|
||||
* @param {any} err Any error encountered
|
||||
* @param {number} ms Ping time in ms
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a ping
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.send = function (callback) {
|
||||
let self = this;
|
||||
callback = callback || function (err, ms) {
|
||||
@@ -157,8 +165,10 @@ Ping.prototype.send = function (callback) {
|
||||
}
|
||||
};
|
||||
|
||||
// CALL Ping#send(callback) ON A TIMER
|
||||
// ===================================
|
||||
/**
|
||||
* Ping every interval
|
||||
* @param {pingCB} callback Callback to call with results
|
||||
*/
|
||||
Ping.prototype.start = function (callback) {
|
||||
let self = this;
|
||||
this._i = setInterval(function () {
|
||||
@@ -167,8 +177,7 @@ Ping.prototype.start = function (callback) {
|
||||
self.send(callback);
|
||||
};
|
||||
|
||||
// STOP SENDING PINGS
|
||||
// ==================
|
||||
/** Stop sending pings */
|
||||
Ping.prototype.stop = function () {
|
||||
clearInterval(this._i);
|
||||
};
|
||||
@@ -177,7 +186,7 @@ Ping.prototype.stop = function () {
|
||||
* 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
|
||||
* @param {any} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function convertOutput(data) {
|
||||
|
@@ -33,8 +33,11 @@ const monitorStatus = new PrometheusClient.Gauge({
|
||||
});
|
||||
|
||||
class Prometheus {
|
||||
monitorLabelValues = {}
|
||||
monitorLabelValues = {};
|
||||
|
||||
/**
|
||||
* @param {Object} monitor Monitor object to monitor
|
||||
*/
|
||||
constructor(monitor) {
|
||||
this.monitorLabelValues = {
|
||||
monitor_name: monitor.name,
|
||||
@@ -45,6 +48,11 @@ class Prometheus {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metrics page
|
||||
* @param {Object} heartbeat Heartbeat details
|
||||
* @param {Object} tlsInfo TLS details
|
||||
*/
|
||||
update(heartbeat, tlsInfo) {
|
||||
|
||||
if (typeof tlsInfo !== "undefined") {
|
||||
|
@@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
|
||||
class Proxy {
|
||||
|
||||
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ]
|
||||
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
|
||||
|
||||
/**
|
||||
* Saves and updates given proxy entity
|
||||
|
@@ -2,11 +2,26 @@ const { RateLimiter } = require("limiter");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
class KumaRateLimiter {
|
||||
/**
|
||||
* @param {Object} config Rate limiter configuration object
|
||||
*/
|
||||
constructor(config) {
|
||||
this.errorMessage = config.errorMessage;
|
||||
this.rateLimiter = new RateLimiter(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for pass
|
||||
* @callback passCB
|
||||
* @param {Object} err Too many requests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Should the request be passed through
|
||||
* @param {passCB} callback
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async pass(callback, num = 1) {
|
||||
const remainingRequests = await this.removeTokens(num);
|
||||
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
||||
@@ -22,6 +37,11 @@ class KumaRateLimiter {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a given number of tokens
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async removeTokens(num = 1) {
|
||||
return await this.rateLimiter.removeTokens(num);
|
||||
}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
let express = require("express");
|
||||
const { allowDevAllOrigin } = require("../util-server");
|
||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, flipStatus, log } = require("../../src/util");
|
||||
const { UP, DOWN, flipStatus, log } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
let cache = apicache.middleware;
|
||||
@@ -34,6 +37,8 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping || null;
|
||||
let statusString = request.query.status || "up";
|
||||
let status = (statusString === "up") ? UP : DOWN;
|
||||
|
||||
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||
pushToken
|
||||
@@ -45,7 +50,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
|
||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||
|
||||
let status = UP;
|
||||
if (monitor.isUpsideDown()) {
|
||||
status = flipStatus(status);
|
||||
}
|
||||
@@ -196,6 +200,187 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
upLabel = "Up",
|
||||
downLabel = "Down",
|
||||
upColor = badgeConstants.defaultUpColor,
|
||||
downColor = badgeConstants.defaultDownColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
const overrideValue = value !== undefined ? parseInt(value) : undefined;
|
||||
|
||||
let publicMonitor = await R.getRow(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND monitor_group.monitor_id = ?
|
||||
AND public = 1
|
||||
`,
|
||||
[ requestedMonitorId ]
|
||||
);
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicMonitor) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||
|
||||
badgeValues.color = state ? upColor : downColor;
|
||||
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||
}
|
||||
|
||||
// build the svg based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
labelPrefix,
|
||||
labelSuffix = badgeConstants.defaultUptimeLabelSuffix,
|
||||
prefix,
|
||||
suffix = badgeConstants.defaultUptimeValueSuffix,
|
||||
color,
|
||||
labelColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
// if no duration is given, set value to 24 (h)
|
||||
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
let publicMonitor = await R.getRow(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND monitor_group.monitor_id = ?
|
||||
AND public = 1
|
||||
`,
|
||||
[ requestedMonitorId ]
|
||||
);
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicMonitor) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const uptime = overrideValue ?? await Monitor.calcUptime(
|
||||
requestedDuration,
|
||||
requestedMonitorId
|
||||
);
|
||||
|
||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||
|
||||
// use a given, custom color or calculate one based on the uptime value
|
||||
badgeValues.color = color ?? percentageToColor(uptime);
|
||||
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||
badgeValues.labelColor = labelColor ?? "";
|
||||
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response) => {
|
||||
allowAllOrigin(response);
|
||||
|
||||
const {
|
||||
label,
|
||||
labelPrefix,
|
||||
labelSuffix = badgeConstants.defaultPingLabelSuffix,
|
||||
prefix,
|
||||
suffix = badgeConstants.defaultPingValueSuffix,
|
||||
color = badgeConstants.defaultPingColor,
|
||||
labelColor,
|
||||
style = badgeConstants.defaultStyle,
|
||||
value, // for demo purpose only
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
|
||||
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
const publicAvgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||
AND heartbeat.ping IS NOT NULL
|
||||
AND public = 1
|
||||
AND heartbeat.monitor_id = ?
|
||||
`,
|
||||
[ -requestedDuration, requestedMonitorId ]
|
||||
));
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!publicAvgPing) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const avgPing = parseInt(overrideValue ?? publicAvgPing);
|
||||
|
||||
badgeValues.color = color;
|
||||
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
|
||||
badgeValues.labelColor = labelColor ?? "";
|
||||
// build a lable string. If a custom label is given, override the default one (requestedDuration)
|
||||
badgeValues.label = filterAndJoin([ labelPrefix, label ?? requestedDuration, labelSuffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Send a 403 response
|
||||
* @param {Object} res Express response object
|
||||
* @param {string} [msg=""] Message to send
|
||||
*/
|
||||
function send403(res, msg = "") {
|
||||
res.status(403).json({
|
||||
"status": "fail",
|
||||
|
106
server/server.js
106
server/server.js
@@ -60,7 +60,7 @@ log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog, doubleCheckPassword } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@@ -136,13 +136,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
let totalClient = 0;
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
@@ -248,17 +241,11 @@ try {
|
||||
|
||||
sendInfo(socket);
|
||||
|
||||
totalClient++;
|
||||
|
||||
if (needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
socket.emit("setup");
|
||||
}
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
totalClient--;
|
||||
});
|
||||
|
||||
// ***************************
|
||||
// Public Socket API
|
||||
// ***************************
|
||||
@@ -327,7 +314,7 @@ try {
|
||||
let user = await login(data.username, data.password);
|
||||
|
||||
if (user) {
|
||||
if (user.twofa_status == 0) {
|
||||
if (user.twofa_status === 0) {
|
||||
afterLogin(socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
@@ -340,7 +327,7 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
if (user.twofa_status == 1 && !data.token) {
|
||||
if (user.twofa_status === 1 && !data.token) {
|
||||
|
||||
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
|
||||
@@ -417,7 +404,7 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_status == 0) {
|
||||
if (user.twofa_status === 0) {
|
||||
let newSecret = genSecret();
|
||||
let encodedSecret = base32.encode(newSecret);
|
||||
|
||||
@@ -548,7 +535,7 @@ try {
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
if (user.twofa_status == 1) {
|
||||
if (user.twofa_status === 1) {
|
||||
callback({
|
||||
ok: true,
|
||||
status: true,
|
||||
@@ -1060,7 +1047,13 @@ try {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (data.disableAuth) {
|
||||
// If currently is disabled auth, don't need to check
|
||||
// Disabled Auth + Want to Disable Auth => No Check
|
||||
// Disabled Auth + Want to Enable Auth => No Check
|
||||
// Enabled Auth + Want to Disable Auth => Check!!
|
||||
// Enabled Auth + Want to Enable Auth => No Check
|
||||
const currentDisabledAuth = await setting("disableAuth");
|
||||
if (!currentDisabledAuth && data.disableAuth) {
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
@@ -1169,7 +1162,7 @@ try {
|
||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||
|
||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
||||
if (importHandle == "overwrite") {
|
||||
if (importHandle === "overwrite") {
|
||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
@@ -1193,7 +1186,7 @@ try {
|
||||
|
||||
for (let i = 0; i < notificationListData.length; i++) {
|
||||
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
||||
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
let notification = JSON.parse(notificationListData[i].config);
|
||||
await Notification.save(notification, null, socket.userID);
|
||||
@@ -1228,7 +1221,7 @@ try {
|
||||
|
||||
for (let i = 0; i < monitorListData.length; i++) {
|
||||
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
|
||||
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
||||
// --- Start ---
|
||||
@@ -1325,7 +1318,7 @@ try {
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
// If monitor was active start it immediately, otherwise pause it
|
||||
if (monitorListData[i].active == 1) {
|
||||
if (monitorListData[i].active === 1) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
} else {
|
||||
await pauseMonitor(socket.userID, bean.id);
|
||||
@@ -1473,11 +1466,11 @@ try {
|
||||
})();
|
||||
|
||||
/**
|
||||
* Adds or removes notifications from a monitor.
|
||||
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
|
||||
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Update notifications for a given monitor
|
||||
* @param {number} monitorID ID of monitor to update
|
||||
* @param {number[]} notificationIDList List of new notification
|
||||
* providers to add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
@@ -1495,11 +1488,11 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks if the user owns a monitor with the given ID.
|
||||
* @param {number} monitorID - The ID of the monitor to check ownership for.
|
||||
* @param {number} userID - The ID of the user who is trying to access this data.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Check if a given user owns a specific monitor
|
||||
* @param {number} userID
|
||||
* @param {number} monitorID
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} The specified user does not own the monitor
|
||||
*/
|
||||
async function checkOwner(userID, monitorID) {
|
||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
@@ -1513,8 +1506,11 @@ async function checkOwner(userID, monitorID) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after user login
|
||||
* This function is used to send the heartbeat list of a monitor.
|
||||
* @param {Socket} socket - The socket object that will be used to send the data.
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {Object} user User object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function afterLogin(socket, user) {
|
||||
socket.userID = user.id;
|
||||
@@ -1542,9 +1538,10 @@ async function afterLogin(socket, user) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database and patch it if necessary.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Initialize the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* started in test mode?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
@@ -1581,11 +1578,10 @@ async function initDatabase(testMode = false) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to resume.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Start the specified monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
@@ -1609,16 +1605,21 @@ async function startMonitor(userID, monitorID) {
|
||||
monitor.start(io);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a given monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function restartMonitor(userID, monitorID) {
|
||||
return await startMonitor(userID, monitorID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to pause.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* Pause a given monitor
|
||||
* @param {number} userID ID of user who owns monitor
|
||||
* @param {number} monitorID ID of monitor to start
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
@@ -1635,9 +1636,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume active monitors
|
||||
*/
|
||||
/** Resume active monitors */
|
||||
async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ");
|
||||
|
||||
@@ -1653,10 +1652,10 @@ async function startMonitors() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the application
|
||||
* Stops all monitors and closes the database connection.
|
||||
* @param {string} signal The signal that triggered this function to be called.
|
||||
*
|
||||
* Generated by Trelent
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function shutdownFunction(signal) {
|
||||
log.info("server", "Shutdown requested");
|
||||
@@ -1678,6 +1677,7 @@ function getClientIp(socket) {
|
||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
||||
}
|
||||
|
||||
/** Final function called before application exits */
|
||||
function finalFunction() {
|
||||
log.info("server", "Graceful shutdown successful!");
|
||||
}
|
||||
@@ -1694,6 +1694,6 @@ gracefulShutdown(server.httpServer, {
|
||||
// Catch unexpected errors here
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
errorLog(error, false);
|
||||
UptimeKumaServer.errorLog(error, false);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
});
|
||||
|
@@ -6,15 +6,28 @@ const io = UptimeKumaServer.getInstance().io;
|
||||
const prefix = "cloudflared_";
|
||||
const cloudflared = new CloudflaredTunnel();
|
||||
|
||||
/**
|
||||
* Change running state
|
||||
* @param {string} running Is it running?
|
||||
* @param {string} message Message to pass
|
||||
*/
|
||||
cloudflared.change = (running, message) => {
|
||||
io.to("cloudflared").emit(prefix + "running", running);
|
||||
io.to("cloudflared").emit(prefix + "message", message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit an error message
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
cloudflared.error = (errorMessage) => {
|
||||
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for cloudflared
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
socket.on(prefix + "join", async () => {
|
||||
@@ -69,6 +82,10 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically start cloudflared
|
||||
* @param {string} token Cloudflared tunnel token
|
||||
*/
|
||||
module.exports.autoStart = async (token) => {
|
||||
if (!token) {
|
||||
token = await setting("cloudflaredTunnelToken");
|
||||
@@ -85,6 +102,7 @@ module.exports.autoStart = async (token) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Stop cloudflared */
|
||||
module.exports.stop = async () => {
|
||||
console.log("Stop cloudflared");
|
||||
if (cloudflared) {
|
||||
|
@@ -1,6 +1,10 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const Database = require("../database");
|
||||
|
||||
/**
|
||||
* Handlers for database
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
|
@@ -4,6 +4,10 @@ const { sendProxyList } = require("../client");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
/**
|
||||
* Handlers for proxy
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.proxySocketHandler = (socket) => {
|
||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||
try {
|
||||
|
@@ -8,6 +8,10 @@ const apicache = require("../modules/apicache");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
/**
|
||||
* Socket handlers for status page
|
||||
* @param {Socket} socket Socket.io instance to add listeners on
|
||||
*/
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
// Post or edit incident
|
||||
@@ -338,6 +342,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
/**
|
||||
* Check slug a-z, 0-9, - only
|
||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||
* @param {string} slug Slug to test
|
||||
*/
|
||||
function checkSlug(slug) {
|
||||
if (typeof slug !== "string") {
|
||||
|
@@ -5,13 +5,14 @@ const http = require("http");
|
||||
const { Server } = require("socket.io");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../src/util");
|
||||
const Database = require("./database");
|
||||
const util = require("util");
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
* @type {UptimeKumaServer}
|
||||
*/
|
||||
class UptimeKumaServer {
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {UptimeKumaServer}
|
||||
@@ -83,6 +84,32 @@ class UptimeKumaServer {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write error to log file
|
||||
* @param {any} error The error to write
|
||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||
*/
|
||||
static errorLog(error, outputToConsole = true) {
|
||||
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
|
||||
errorLogStream.on("error", () => {
|
||||
log.info("", "Cannot write to error.log");
|
||||
});
|
||||
|
||||
if (errorLogStream) {
|
||||
const dateTime = R.isoDateTime();
|
||||
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
|
||||
|
||||
if (outputToConsole) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
errorLogStream.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -7,9 +7,9 @@ const { Resolver } = require("dns");
|
||||
const childProcess = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const fs = require("fs");
|
||||
const nodeJsUtil = require("util");
|
||||
const mqtt = require("mqtt");
|
||||
const chroma = require("chroma-js");
|
||||
const { badgeConstants } = require("./config");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
@@ -37,6 +37,12 @@ exports.initJWTSecret = async () => {
|
||||
return jwtSecretBean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send TCP request to specified hostname and port
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
* @param {number} port TCP port to test
|
||||
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
|
||||
*/
|
||||
exports.tcping = function (hostname, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tcpp.ping({
|
||||
@@ -58,6 +64,11 @@ exports.tcping = function (hostname, port) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ping the specified machine
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||
*/
|
||||
exports.ping = async (hostname) => {
|
||||
try {
|
||||
return await exports.pingAsync(hostname);
|
||||
@@ -71,6 +82,12 @@ exports.ping = async (hostname) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ping the specified machine
|
||||
* @param {string} hostname Hostname / address of machine to ping
|
||||
* @param {boolean} ipv6 Should IPv6 be used?
|
||||
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
|
||||
*/
|
||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ping = new Ping(hostname, {
|
||||
@@ -89,6 +106,15 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* MQTT Monitor
|
||||
* @param {string} hostname Hostname / address of machine to test
|
||||
* @param {string} topic MQTT topic
|
||||
* @param {string} okMessage Expected result
|
||||
* @param {Object} [options={}] MQTT options. Contains port, username,
|
||||
* password and interval (interval defaults to 20)
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
@@ -132,7 +158,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
});
|
||||
|
||||
client.on("message", (messageTopic, message) => {
|
||||
if (messageTopic == topic) {
|
||||
if (messageTopic === topic) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
|
||||
@@ -146,6 +172,13 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a given record using the specified DNS server
|
||||
* @param {string} hostname The hostname of the record to lookup
|
||||
* @param {string} resolverServer The DNS server to use
|
||||
* @param {string} rrtype The type of record to request
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
*/
|
||||
exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
||||
const resolver = new Resolver();
|
||||
resolver.setServers([ resolverServer ]);
|
||||
@@ -170,6 +203,11 @@ exports.dnsResolve = function (hostname, resolverServer, rrtype) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve value of setting based on key
|
||||
* @param {string} key Key of setting to retrieve
|
||||
* @returns {Promise<any>} Value
|
||||
*/
|
||||
exports.setting = async function (key) {
|
||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||
key,
|
||||
@@ -184,6 +222,13 @@ exports.setting = async function (key) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the specified setting to specifed value
|
||||
* @param {string} key Key of setting to set
|
||||
* @param {any} value Value to set to
|
||||
* @param {?string} type Type of setting
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setSetting = async function (key, value, type = null) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key,
|
||||
@@ -197,6 +242,11 @@ exports.setSetting = async function (key, value, type = null) {
|
||||
await R.store(bean);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get settings based on type
|
||||
* @param {?string} type The type of setting
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
type,
|
||||
@@ -215,6 +265,12 @@ exports.getSettings = async function (type) {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set settings based on type
|
||||
* @param {?string} type Type of settings to set
|
||||
* @param {Object} data Values of settings
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setSettings = async function (type, data) {
|
||||
let keyList = Object.keys(data);
|
||||
|
||||
@@ -241,12 +297,23 @@ exports.setSettings = async function (type, data) {
|
||||
};
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
// return an object containing the certificate information
|
||||
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
|
||||
|
||||
/**
|
||||
* Get number of days between two dates
|
||||
* @param {Date} validFrom Start date
|
||||
* @param {Date} validTo End date
|
||||
* @returns {number}
|
||||
*/
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
|
||||
/**
|
||||
* Get days remaining from a time range
|
||||
* @param {Date} validFrom Start date
|
||||
* @param {Date} validTo End date
|
||||
* @returns {number}
|
||||
*/
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
@@ -255,8 +322,11 @@ const getDaysRemaining = (validFrom, validTo) => {
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
// Fix certificate Info for display
|
||||
// param: info - the chain obtained from getPeerCertificate()
|
||||
/**
|
||||
* Fix certificate info for display
|
||||
* @param {Object} info The chain obtained from getPeerCertificate()
|
||||
* @returns {Object} An object representing certificate information
|
||||
*/
|
||||
const parseCertificateInfo = function (info) {
|
||||
let link = info;
|
||||
let i = 0;
|
||||
@@ -296,6 +366,11 @@ const parseCertificateInfo = function (info) {
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if certificate is valid
|
||||
* @param {Object} res Response object from axios
|
||||
* @returns {Object} Object containing certificate information
|
||||
*/
|
||||
exports.checkCertificate = function (res) {
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
@@ -309,12 +384,13 @@ exports.checkCertificate = function (res) {
|
||||
};
|
||||
};
|
||||
|
||||
// Check if the provided status code is within the accepted ranges
|
||||
// Param: status - the status code to check
|
||||
// Param: accepted_codes - an array of accepted status codes
|
||||
// Return: true if the status code is within the accepted ranges, false otherwise
|
||||
// Will throw an error if the provided status code is not a valid range string or code string
|
||||
|
||||
/**
|
||||
* Check if the provided status code is within the accepted ranges
|
||||
* @param {string} status The status code to check
|
||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||
* @returns {boolean} True if status code within range, false otherwise
|
||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||
*/
|
||||
exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||
return false;
|
||||
@@ -338,6 +414,12 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get total number of clients in room
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {string} roomName Name of room to check
|
||||
* @returns {number}
|
||||
*/
|
||||
exports.getTotalClientInRoom = (io, roomName) => {
|
||||
|
||||
const sockets = io.sockets;
|
||||
@@ -361,17 +443,29 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow CORS all origins if development
|
||||
* @param {Object} res Response object from axios
|
||||
*/
|
||||
exports.allowDevAllOrigin = (res) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
exports.allowAllOrigin(res);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow CORS all origins
|
||||
* @param {Object} res Response object from axios
|
||||
*/
|
||||
exports.allowAllOrigin = (res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a user is logged in
|
||||
* @param {Socket} socket Socket instance
|
||||
*/
|
||||
exports.checkLogin = (socket) => {
|
||||
if (!socket.userID) {
|
||||
throw new Error("You are not logged in.");
|
||||
@@ -380,8 +474,8 @@ exports.checkLogin = (socket) => {
|
||||
|
||||
/**
|
||||
* For logged-in users, double-check the password
|
||||
* @param socket
|
||||
* @param currentPassword
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {string} currentPassword
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
@@ -400,6 +494,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
return user;
|
||||
};
|
||||
|
||||
/** Start Unit tests */
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
@@ -420,7 +515,8 @@ exports.startUnitTest = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param body : Buffer
|
||||
* Convert unknown string to UTF8
|
||||
* @param {Uint8Array} body Buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.convertToUTF8 = (body) => {
|
||||
@@ -429,23 +525,32 @@ exports.convertToUTF8 = (body) => {
|
||||
return str.toString();
|
||||
};
|
||||
|
||||
let logFile;
|
||||
|
||||
try {
|
||||
logFile = fs.createWriteStream("./data/error.log", {
|
||||
flags: "a"
|
||||
});
|
||||
} catch (_) { }
|
||||
|
||||
exports.errorLog = (error, outputToConsole = true) => {
|
||||
/**
|
||||
* Returns a color code in hex format based on a given percentage:
|
||||
* 0% => hue = 10 => red
|
||||
* 100% => hue = 90 => green
|
||||
*
|
||||
* @param {number} percentage float, 0 to 1
|
||||
* @param {number} maxHue
|
||||
* @param {number} minHue, int
|
||||
* @returns {string}, hex value
|
||||
*/
|
||||
exports.percentageToColor = (percentage, maxHue = 90, minHue = 10) => {
|
||||
const hue = percentage * (maxHue - minHue) + minHue;
|
||||
try {
|
||||
if (logFile) {
|
||||
const dateTime = R.isoDateTime();
|
||||
logFile.write(`[${dateTime}] ` + nodeJsUtil.format(error) + "\n");
|
||||
|
||||
if (outputToConsole) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} catch (_) { }
|
||||
return chroma(`hsl(${hue}, 90%, 40%)`).hex();
|
||||
} catch (err) {
|
||||
return badgeConstants.naColor;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Joins and array of string to one string after filtering out empty values
|
||||
*
|
||||
* @param {string[]} parts
|
||||
* @param {string} connector
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.filterAndJoin = (parts, connector = "") => {
|
||||
return parts.filter((part) => !!part && part !== "").join(connector);
|
||||
};
|
||||
|
@@ -367,7 +367,7 @@ textarea.form-control {
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 13px 15px 10px 15px;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
|
@@ -10,7 +10,10 @@ import { sleep } from "../util.ts";
|
||||
export default {
|
||||
|
||||
props: {
|
||||
value: [ String, Number ],
|
||||
value: {
|
||||
type: [ String, Number ],
|
||||
default: 0,
|
||||
},
|
||||
time: {
|
||||
type: Number,
|
||||
default: 0.3,
|
||||
|
@@ -5,15 +5,18 @@
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
dateOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrap" class="wrap" :style="wrapStyle">
|
||||
<div class="hp-bar-big" :style="barStyle">
|
||||
<div class="hp-bar-big d-flex" :style="barStyle">
|
||||
<div
|
||||
v-for="(beat, index) in shortBeatList"
|
||||
:key="index"
|
||||
@@ -8,7 +8,11 @@
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||
:style="beatStyle"
|
||||
:title="getBeatTitle(beat)"
|
||||
/>
|
||||
@mouseenter="toggleActivateSibling"
|
||||
@mouseleave="toggleActivateSibling"
|
||||
>
|
||||
<div class="beat-inner" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -168,6 +172,29 @@ export default {
|
||||
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||
},
|
||||
|
||||
// Toggling the activeSibling class on hover over the current hover item
|
||||
toggleActivateSibling(e) {
|
||||
// Variable definition
|
||||
const element = e.target;
|
||||
const previous = element.previousSibling;
|
||||
const next = element.nextSibling;
|
||||
|
||||
// Return if the hovered element has empty class
|
||||
if (element.classList.contains("empty")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Previous Sibling is heartbar element and doesn't have the empty class
|
||||
if (previous.children && !previous.classList.contains("empty")) {
|
||||
previous.classList.toggle("active-sibling");
|
||||
}
|
||||
|
||||
// Check if Next Sibling is heartbar element and doesn't have the empty class
|
||||
if (next.children && !next.classList.contains("empty")) {
|
||||
next.classList.toggle("active-sibling");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -184,9 +211,10 @@ export default {
|
||||
|
||||
.hp-bar-big {
|
||||
.beat {
|
||||
display: inline-block;
|
||||
background-color: $primary;
|
||||
border-radius: $border-radius;
|
||||
display: inline-block;
|
||||
transition: all ease 0.6s;
|
||||
|
||||
&.empty {
|
||||
background-color: aliceblue;
|
||||
@@ -200,11 +228,23 @@ export default {
|
||||
background-color: $warning;
|
||||
}
|
||||
|
||||
.beat-inner {
|
||||
border-radius: $border-radius;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&:not(.empty):hover {
|
||||
transition: all ease-in-out 0.15s;
|
||||
transition: all ease 0.15s;
|
||||
opacity: 0.8;
|
||||
transform: scale(var(--hover-scale));
|
||||
}
|
||||
|
||||
&.active-sibling {
|
||||
transform: scale(1.15);
|
||||
transition: all ease 0.15s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -47,8 +47,8 @@
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText != "") {
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(monitor => {
|
||||
return monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
@@ -170,12 +170,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.footer {
|
||||
// background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.list-header {
|
||||
margin: -20px;
|
||||
|
@@ -18,13 +18,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import "chartjs-adapter-dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { LineChart } from "vue-chart-3";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { DOWN } from "../util.ts";
|
||||
import { DOWN, log } from "../util.ts";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
@@ -217,8 +217,9 @@ export default {
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
newPeriod = null;
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
} else {
|
||||
@@ -241,7 +242,11 @@ export default {
|
||||
// And mirror latest change to this.heartbeatList
|
||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||
(heartbeatList) => {
|
||||
if (this.chartPeriodHrs != 0) {
|
||||
|
||||
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.chartPeriodHrs != "0") {
|
||||
const newBeat = heartbeatList.at(-1);
|
||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||
this.heartbeatList.push(heartbeatList.at(-1));
|
||||
|
@@ -33,19 +33,19 @@
|
||||
<template #item="monitor">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<div class="col-9 col-md-8 small-padding d-flex align-items-center flex-wrap">
|
||||
<div class="info d-flex align-items-center gap-3 w-100">
|
||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||
|
||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||
{{ monitor.element.name }}
|
||||
</div>
|
||||
<div v-if="showTags" class="tags">
|
||||
<div v-if="showTags && monitor.element.tags.length > 0" class="tags">
|
||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
</div>
|
||||
</div>
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4 d-flex align-items-center">
|
||||
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -5,7 +5,10 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
status: Number,
|
||||
status: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div class="tag-wrapper rounded d-inline-flex"
|
||||
:class="{ 'px-3': size == 'normal',
|
||||
'py-1': size == 'normal',
|
||||
'm-2': size == 'normal',
|
||||
'px-2': size == 'sm',
|
||||
'py-0': size == 'sm',
|
||||
'm-1': size == 'sm',
|
||||
}"
|
||||
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||
<div
|
||||
class="tag-wrapper rounded d-inline-flex"
|
||||
:class="{ 'px-3': size == 'normal',
|
||||
'py-1': size == 'normal',
|
||||
'm-2': size == 'normal',
|
||||
'px-2': size == 'sm',
|
||||
'py-0': size == 'sm',
|
||||
'm-1': size == 'sm',
|
||||
}"
|
||||
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||
>
|
||||
<span class="tag-text">{{ displayText }}</span>
|
||||
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||
@@ -34,7 +35,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
displayText() {
|
||||
if (this.item.value == "") {
|
||||
if (this.item.value === "") {
|
||||
return this.item.name;
|
||||
} else {
|
||||
return `${this.item.name}: ${this.item.value}`;
|
||||
|
@@ -34,18 +34,20 @@
|
||||
label="name"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
<div
|
||||
class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>
|
||||
{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #singleLabel="{ option }">
|
||||
<div class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
<div
|
||||
class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px;"
|
||||
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
@@ -53,10 +55,11 @@
|
||||
</vue-multiselect>
|
||||
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
||||
<div class="w-50 pe-2">
|
||||
<input v-model="newDraftTag.name" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||
:placeholder="$t('Name')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
<input
|
||||
v-model="newDraftTag.name" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||
:placeholder="$t('Name')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("Tag with this name already exist.") }}
|
||||
@@ -75,17 +78,19 @@
|
||||
deselect-label=""
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
<div
|
||||
class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #singleLabel="{ option }">
|
||||
<div class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
<div
|
||||
class="py-1 px-3 rounded d-inline-flex"
|
||||
style="height: 24px; color: white;"
|
||||
:style="{ backgroundColor: option.color + ' !important' }"
|
||||
>
|
||||
<span>{{ option.name }}</span>
|
||||
</div>
|
||||
@@ -94,10 +99,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input v-model="newDraftTag.value" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||
:placeholder="$t('value (optional)')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
<input
|
||||
v-model="newDraftTag.value" class="form-control"
|
||||
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||
:placeholder="$t('value (optional)')"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
/>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("Tag with this value already exist.") }}
|
||||
@@ -123,8 +129,8 @@
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
import Tag from "../components/Tag.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
@@ -159,14 +165,14 @@ export default {
|
||||
tagOptions() {
|
||||
const tagOptions = this.existingTags;
|
||||
for (const tag of this.newTags) {
|
||||
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
||||
if (!tagOptions.find(t => t.name === tag.name && t.color === tag.color)) {
|
||||
tagOptions.push(tag);
|
||||
}
|
||||
}
|
||||
return tagOptions;
|
||||
},
|
||||
selectedTags() {
|
||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
||||
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
|
||||
},
|
||||
colorOptions() {
|
||||
return [
|
||||
@@ -192,7 +198,7 @@ export default {
|
||||
let nameInvalid = false;
|
||||
let valueInvalid = false;
|
||||
let invalid = true;
|
||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
||||
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value)) {
|
||||
// Undo removing a Tag
|
||||
nameInvalid = false;
|
||||
valueInvalid = false;
|
||||
@@ -202,9 +208,9 @@ export default {
|
||||
nameInvalid = true;
|
||||
invalid = true;
|
||||
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
||||
tag.name === this.newDraftTag.select?.name && tag.value === this.newDraftTag.value
|
||||
) || (
|
||||
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
||||
tag.name === this.newDraftTag.name && tag.value === this.newDraftTag.value
|
||||
)).length > 0) {
|
||||
// Try to add a tag with existing name and value
|
||||
valueInvalid = true;
|
||||
@@ -250,7 +256,7 @@ export default {
|
||||
deleteTag(item) {
|
||||
if (item.new) {
|
||||
// Undo Adding a new Tag
|
||||
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
|
||||
this.newTags = this.newTags.filter(tag => !(tag.name === item.name && tag.value === item.value));
|
||||
} else {
|
||||
// Remove an Existing Tag
|
||||
this.deleteTags.push(item);
|
||||
@@ -266,9 +272,9 @@ export default {
|
||||
addDraftTag() {
|
||||
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||
if (this.newDraftTag.select != null) {
|
||||
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
||||
if (this.deleteTags.find(tag => tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value)) {
|
||||
// Undo removing a tag
|
||||
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
||||
this.deleteTags = this.deleteTags.filter(tag => !(tag.name === this.newDraftTag.select.name && tag.value === this.newDraftTag.value));
|
||||
} else {
|
||||
// Add an existing Tag
|
||||
this.newTags.push({
|
||||
@@ -345,7 +351,7 @@ export default {
|
||||
tagId = newTagResult.id;
|
||||
// Assign the new ID to the tags of the same name & color
|
||||
this.newTags.map(tag => {
|
||||
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||
if (tag.name === newTag.name && tag.color === newTag.color) {
|
||||
tag.id = newTagResult.id;
|
||||
}
|
||||
});
|
||||
|
@@ -5,8 +5,14 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
monitor: Object,
|
||||
type: String,
|
||||
monitor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
pill: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@@ -8,6 +8,9 @@
|
||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||
<input id="title" v-model="$parent.notification.title" type="text" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<i18n-t tag="p" keypath="Status:">
|
||||
|
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-login" class="form-label">API Username</label>
|
||||
<div class="form-text">
|
||||
{{ $t("apiCredentials") }}
|
||||
<label for="clicksendsms-login" class="form-label">{{ $t("API Username") }}</label>
|
||||
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
|
||||
<a href="http://dashboard.clicksend.com/account/subaccounts" target="_blank">{{ $t("here") }}</a>
|
||||
</div>
|
||||
</i18n-t>
|
||||
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
|
||||
<label for="clicksendsms-key" class="form-label">API Key</label>
|
||||
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
|
||||
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -16,15 +15,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-to-number" class="form-label">Recipient Number</label>
|
||||
<label for="clicksendsms-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
|
||||
<input id="clicksendsms-to-number" v-model="$parent.notification.clicksendsmsToNumber" type="text" minlength="8" maxlength="14" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="clicksendsms-sender-name" class="form-label">From Name/Number -
|
||||
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">More Info</a>
|
||||
<label for="clicksendsms-sender-name" class="form-label">{{ $t("From Name/Number") }} -
|
||||
<a href="https://help.clicksend.com/article/4kgj7krx00-what-is-a-sender-id-or-sender-number" target="_blank">{{ $t("Read more") }}</a>
|
||||
</label>
|
||||
<input id="clicksendsms-sender-name" v-model="$parent.notification.clicksendsmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||
<div class="form-text">Leave blank to use a shared sender number.</div>
|
||||
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<b>{{ $t("Basic Settings") }}</b>
|
||||
</i18n-t>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<label for="line-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
|
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-version" class="form-label">Octopush API Version</label>
|
||||
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
|
||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||
<option value="2">Octopush (endpoint: api.octopush.com)</option>
|
||||
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
|
||||
<option value="2">{{ $t("octopush") }} ({{ $t("endpoint") }}: api.octopush.com)</option>
|
||||
<option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("octopushLegacyHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="octopush-key" class="form-label">API KEY</label>
|
||||
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
|
||||
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
|
||||
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="promosms-login" class="form-label">API LOGIN</label>
|
||||
<label for="promosms-login" class="form-label">{{ $("promosmsLogin") }}</label>
|
||||
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
|
||||
<label for="promosms-key" class="form-label">API PASSWORD</label>
|
||||
<label for="promosms-key" class="form-label">{{ $("promosmsPassword") }}</label>
|
||||
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@@ -18,28 +18,29 @@
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">{{ $t("Notification Sound") }}</label>
|
||||
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
<option value="pushover">{{ $t("pushoversounds pushover") }}</option>
|
||||
<option value="bike">{{ $t("pushoversounds bike") }}</option>
|
||||
<option value="bugle">{{ $t("pushoversounds bugle") }}</option>
|
||||
<option value="cashregister">{{ $t("pushoversounds cashregister") }}</option>
|
||||
<option value="classical">{{ $t("pushoversounds classical") }}</option>
|
||||
<option value="cosmic">{{ $t("pushoversounds cosmic") }}</option>
|
||||
<option value="falling">{{ $t("pushoversounds falling") }}</option>
|
||||
<option value="gamelan">{{ $t("pushoversounds gamelan") }}</option>
|
||||
<option value="incoming">{{ $t("pushoversounds incoming") }}</option>
|
||||
<option value="intermission">{{ $t("pushoversounds intermission") }}</option>
|
||||
<option value="magic">{{ $t("pushoversounds magic") }}</option>
|
||||
<option value="mechanical">{{ $t("pushoversounds mechanical") }}</option>
|
||||
<option value="pianobar">{{ $t("pushoversounds pianobar") }}</option>
|
||||
<option value="siren">{{ $t("pushoversounds siren") }}</option>
|
||||
<option value="spacealarm">{{ $t("pushoversounds spacealarm") }}</option>
|
||||
<option value="tugboat">{{ $t("pushoversounds tugboat") }}</option>
|
||||
<option value="alien">{{ $t("pushoversounds alien") }}</option>
|
||||
<option value="climb">{{ $t("pushoversounds climb") }}</option>
|
||||
<option value="persistent">{{ $t("pushoversounds persistent") }}</option>
|
||||
<option value="echo">{{ $t("pushoversounds echo") }}</option>
|
||||
<option value="updown">{{ $t("pushoversounds updown") }}</option>
|
||||
<option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
|
||||
<option value="none">{{ $t("pushoversounds none") }}</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
|
||||
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="push-api-key" class="form-label">API_KEY</label>
|
||||
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
|
||||
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||
</div>
|
||||
|
||||
|
@@ -9,11 +9,11 @@
|
||||
|
||||
<div class="mt-1">
|
||||
<div class="form-check">
|
||||
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
|
||||
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
|
||||
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,15 +12,15 @@ export default {
|
||||
keywordDescription: "Търси ключова дума в чист html или JSON отговор - чувствителна е към регистъра",
|
||||
pauseDashboardHome: "Пауза",
|
||||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
|
||||
deleteNotificationMsg: "Наистина ли желаете да изтриете това известяване за всички монитори?",
|
||||
deleteNotificationMsg: "Наистина ли желаете да изтриете това известие за всички монитори?",
|
||||
resolverserverDescription: "Cloudflare е сървърът по подразбиране, но можете да го промените по всяко време.",
|
||||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
|
||||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
|
||||
enableDefaultNotificationDescription: "За всеки нов монитор това известяване ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
|
||||
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да го изключите за всеки отделен монитор.",
|
||||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
|
||||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
|
||||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
|
||||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известяване със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известяване.",
|
||||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако желаете да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
|
||||
confirmImportMsg: "Сигурни ли сте, че желаете импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
|
||||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
|
||||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
|
||||
@@ -76,9 +76,9 @@ export default {
|
||||
"Max. Redirects": "Макс. брой пренасочвания",
|
||||
"Accepted Status Codes": "Допустими статус кодове",
|
||||
Save: "Запази",
|
||||
Notifications: "Известявания",
|
||||
Notifications: "Известия",
|
||||
"Not available, please setup.": "Не са налични. Моля, настройте.",
|
||||
"Setup Notification": "Настройки за известявания",
|
||||
"Setup Notification": "Настрой известие",
|
||||
Light: "Светла",
|
||||
Dark: "Тъмна",
|
||||
Auto: "Автоматично",
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
Login: "Вход",
|
||||
"No Monitors, please": "Все още няма монитори. Моля, добавете поне ",
|
||||
"add one": "един.",
|
||||
"Notification Type": "Тип известяване",
|
||||
"Notification Type": "Тип известие",
|
||||
Email: "Имейл",
|
||||
Test: "Тест",
|
||||
"Certificate Info": "Информация за сертификат",
|
||||
@@ -131,9 +131,9 @@ export default {
|
||||
Events: "Събития",
|
||||
Heartbeats: "Проверки",
|
||||
"Auto Get": "Авт. попълване",
|
||||
backupDescription: "Можете да архивирате всички монитори и всички известявания в JSON файл.",
|
||||
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
|
||||
backupDescription2: "PS: Имайте предвид, че данните за история и събития няма да бъдат включени.",
|
||||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||
backupDescription3: "Чувствителни данни, като токен кодове за известия, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
|
||||
alertNoFile: "Моля, изберете файл за импортиране.",
|
||||
alertWrongFileType: "Моля, изберете JSON файл.",
|
||||
"Clear all statistics": "Изтрий цялата статистика",
|
||||
@@ -202,7 +202,7 @@ export default {
|
||||
"Push URL": "Генериран Push URL адрес",
|
||||
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
||||
pushOptionalParams: "Допълнителни, но не задължителни параметри: {0}",
|
||||
defaultNotificationName: "Моето {notification} известяване ({number})",
|
||||
defaultNotificationName: "Моето {notification} известие ({number})",
|
||||
here: "тук",
|
||||
Required: "Задължително поле",
|
||||
"Bot Token": "Бот токен",
|
||||
@@ -252,7 +252,7 @@ export default {
|
||||
"Notification Sound": "Звуков сигнал",
|
||||
"More info on:": "Повече информация на: {0}",
|
||||
pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.",
|
||||
pushoverDesc2: "Ако желаете да изпратите известявания до различни устройства, попълнете полето Устройство.",
|
||||
pushoverDesc2: "Ако желаете да изпратите известия до различни устройства, попълнете полето Устройство.",
|
||||
"SMS Type": "SMS тип",
|
||||
octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)",
|
||||
octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)",
|
||||
@@ -275,7 +275,7 @@ export default {
|
||||
lineDevConsoleTo: "Line - Конзола за разработчици - {0}",
|
||||
"Basic Settings": "Основни настройки",
|
||||
"User ID": "Потребител ID",
|
||||
"Messaging API": "API за известяване",
|
||||
"Messaging API": "API за съобщаване",
|
||||
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
|
||||
"Icon URL": "URL адрес за иконка",
|
||||
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
|
||||
@@ -291,7 +291,7 @@ export default {
|
||||
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
|
||||
"Internal Room Id": "ID на вътрешна стая",
|
||||
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известяванията. Токен код за достъп ще получите изпълнявайки {0}",
|
||||
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
|
||||
Method: "Метод",
|
||||
Body: "Съобщение",
|
||||
Headers: "Хедъри",
|
||||
@@ -353,8 +353,8 @@ export default {
|
||||
serwersmsSenderName: "SMS Подател име (регистриран през клиентския портал)",
|
||||
stackfield: "Stackfield",
|
||||
smtpDkimSettings: "DKIM Настройки",
|
||||
smtpDkimDesc: "Моля, вижте Nodemailer DKIM {0} за инструкции.",
|
||||
documentation: "документация",
|
||||
smtpDkimDesc: "Моля, вижте {0} на Nodemailer DKIM за инструкции.",
|
||||
documentation: "документацията",
|
||||
smtpDkimDomain: "Домейн",
|
||||
smtpDkimKeySelector: "Селектор на ключ",
|
||||
smtpDkimPrivateKey: "Частен ключ",
|
||||
@@ -401,7 +401,7 @@ export default {
|
||||
Retry: "Повтори",
|
||||
Topic: "Тема",
|
||||
"WeCom Bot Key": "WeCom бот ключ",
|
||||
"Setup Proxy": "Настройка за прокси",
|
||||
"Setup Proxy": "Настрой прокси",
|
||||
"Proxy Protocol": "Прокси протокол",
|
||||
"Proxy Server": "Прокси сървър",
|
||||
"Proxy server has authentication": "Прокси сървърът е с удостоверяване",
|
||||
@@ -411,8 +411,8 @@ export default {
|
||||
Running: "Работи",
|
||||
"Not running": "Не работи",
|
||||
"Remove Token": "Премахни токен",
|
||||
Start: "Старт",
|
||||
Stop: "Стоп",
|
||||
Start: "Стартирай",
|
||||
Stop: "Спри",
|
||||
"Uptime Kuma": "Uptime Kuma",
|
||||
"Add New Status Page": "Добави нова статус страница",
|
||||
Slug: "Слъг",
|
||||
@@ -449,7 +449,7 @@ export default {
|
||||
Customize: "Персонализирай",
|
||||
"Custom Footer": "Персонализиран долен колонтитул",
|
||||
"Custom CSS": "Потребителски CSS",
|
||||
"Domain Name Expiry Notification": "Известяване при изтичащ домейн",
|
||||
"Domain Name Expiry Notification": "Известие при изтичащ домейн",
|
||||
Proxy: "Прокси",
|
||||
"Date Created": "Дата на създаване",
|
||||
onebotHttpAddress: "OneBot HTTP адрес",
|
||||
|
@@ -179,7 +179,7 @@ export default {
|
||||
"Edit Status Page": "Bearbeite Status-Seite",
|
||||
"Go to Dashboard": "Gehe zum Dashboard",
|
||||
"Status Page": "Status-Seite",
|
||||
"Status Pages": "Status-Seite",
|
||||
"Status Pages": "Status-Seiten",
|
||||
telegram: "Telegram",
|
||||
webhook: "Webhook",
|
||||
smtp: "E-Mail (SMTP)",
|
||||
@@ -403,8 +403,8 @@ export default {
|
||||
"WeCom Bot Key": "WeCom Bot Schlüssel",
|
||||
"Setup Proxy": "Proxy einrichten",
|
||||
"Proxy Protocol": "Proxy Protokoll",
|
||||
"Proxy Server": "Proxy Server",
|
||||
"Proxy server has authentication": "Proxy server hat Authentifizierung",
|
||||
"Proxy Server": "Proxy-Server",
|
||||
"Proxy server has authentication": "Proxy-Server hat Authentifizierung",
|
||||
User: "Benutzer",
|
||||
Installed: "Installiert",
|
||||
"Not installed": "Nicht installiert",
|
||||
@@ -442,7 +442,14 @@ export default {
|
||||
"Issuer:": "Aussteller:",
|
||||
"Fingerprint:": "Fingerabdruck:",
|
||||
"No status pages": "Keine Status-Seiten",
|
||||
"Domain Name Expiry Notification": "Benachrichtigung bei Ablauf des Domainnamens",
|
||||
Customize: "Anpassen",
|
||||
"Custom Footer": "Eigener Footer",
|
||||
"Custom CSS": "Eigenes CSS",
|
||||
"Footer Text": "Fußzeile",
|
||||
"Show Powered By": "Zeige 'Powered By'",
|
||||
"Date Created": "Erstellt am",
|
||||
"Domain Names": "Domainnamen",
|
||||
signedInDisp: "Angemeldet als {0}",
|
||||
signedInDispDisabled: "Authentifizierung deaktiviert.",
|
||||
};
|
||||
|
@@ -464,4 +464,55 @@ export default {
|
||||
"Domain Names": "Domain Names",
|
||||
signedInDisp: "Signed in as {0}",
|
||||
signedInDispDisabled: "Auth Disabled.",
|
||||
"Certificate Expiry Notification": "Certificate Expiry Notification",
|
||||
"API Username": "API Username",
|
||||
"API Key": "API Key",
|
||||
"Recipient Number": "Recipient Number",
|
||||
"From Name/Number": "From Name/Number",
|
||||
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
|
||||
"Octopush API Version": "Octopush API Version",
|
||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
||||
"endpoint": "endpoint",
|
||||
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
|
||||
octopushLogin: "\"Login\" from HTTP API credentials in control panel",
|
||||
promosmsLogin: "API Login Name",
|
||||
promosmsPassword: "API Password",
|
||||
"pushoversounds pushover": "Pushover (default)",
|
||||
"pushoversounds bike": "Bike",
|
||||
"pushoversounds bugle": "Bugle",
|
||||
"pushoversounds cashregister": "Cash Register",
|
||||
"pushoversounds classical": "Classical",
|
||||
"pushoversounds cosmic": "Cosmic",
|
||||
"pushoversounds falling": "Falling",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "Incoming",
|
||||
"pushoversounds intermission": "Intermission",
|
||||
"pushoversounds magic": "Magic",
|
||||
"pushoversounds mechanical": "Mechanical",
|
||||
"pushoversounds pianobar": "Piano Bar",
|
||||
"pushoversounds siren": "Siren",
|
||||
"pushoversounds spacealarm": "Space Alarm",
|
||||
"pushoversounds tugboat": "Tug Boat",
|
||||
"pushoversounds alien": "Alien Alarm (long)",
|
||||
"pushoversounds climb": "Climb (long)",
|
||||
"pushoversounds persistent": "Persistent (long)",
|
||||
"pushoversounds echo": "Pushover Echo (long)",
|
||||
"pushoversounds updown": "Up Down (long)",
|
||||
"pushoversounds vibrate": "Vibrate Only",
|
||||
"pushoversounds none": "None (silent)",
|
||||
pushyAPIKey: "Secret API Key",
|
||||
pushyToken: "Device token",
|
||||
"Show update if available": "Show update if available",
|
||||
"Also check beta release": "Also check beta release",
|
||||
"Using a Reverse Proxy?": "Using a Reverse Proxy?",
|
||||
"Check how to config it for WebSocket": "Check how to config it for WebSocket",
|
||||
"Steam Game Server": "Steam Game Server",
|
||||
"Most likely causes:": "Most likely causes:",
|
||||
"The resource is no longer available.": "The resource is no longer available.",
|
||||
"There might be a typing error in the address.": "There might be a typing error in the address.",
|
||||
"What you can try:": "What you can try:",
|
||||
"Retype the address.": "Retype the address.",
|
||||
"Go back to the previous page.": "Go back to the previous page.",
|
||||
"Coming Soon": "Coming Soon",
|
||||
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
|
||||
};
|
||||
|
@@ -66,7 +66,7 @@ export default {
|
||||
Keyword: "Słowo kluczowe",
|
||||
"Friendly Name": "Przyjazna nazwa",
|
||||
URL: "URL",
|
||||
Hostname: "Hostname",
|
||||
Hostname: "Nazwa hosta",
|
||||
Port: "Port",
|
||||
"Heartbeat Interval": "Częstotliwość bicia serca",
|
||||
Retries: "Prób",
|
||||
@@ -216,7 +216,7 @@ export default {
|
||||
signal: "Signal",
|
||||
Number: "Numer",
|
||||
Recipients: "Odbiorcy",
|
||||
needSignalAPI: "Musisz posiadać klienta Signal z REST API.",
|
||||
needSignalAPI: "Musisz mieć klienta Signal z REST API.",
|
||||
wayToCheckSignalURL: "W celu dowiedzenia się, jak go skonfigurować, odwiedź poniższy link:",
|
||||
signalImportant: "UWAGA: Nie można mieszać nazw grup i numerów odbiorców!",
|
||||
gotify: "Gotify",
|
||||
@@ -234,6 +234,7 @@ export default {
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
PushByTechulus: "Push od Techulus",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
lunasea: "LunaSea",
|
||||
@@ -278,7 +279,7 @@ export default {
|
||||
promosmsTypeEco: "SMS ECO - tanie, lecz wolne. Dostępne tylko w Polsce",
|
||||
promosmsTypeFlash: "SMS FLASH - wiadomość automatycznie wyświetli się na urządzeniu. Dostępne tylko w Polsce.",
|
||||
promosmsTypeFull: "SMS FULL - szybkie i dostępne międzynarodowo. Wersja premium usługi, która pozwala min. ustawić własną nazwę nadawcy.",
|
||||
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, posiada wszystkie zalety SMS FULL",
|
||||
promosmsTypeSpeed: "SMS SPEED - wysyłka priorytetowa, ma wszystkie zalety SMS FULL",
|
||||
promosmsPhoneNumber: "Numer odbiorcy",
|
||||
promosmsSMSSender: "Nadawca SMS (wcześniej zatwierdzone nazwy z panelu PromoSMS)",
|
||||
"Primary Base URL": "Główny URL",
|
||||
@@ -306,6 +307,10 @@ export default {
|
||||
"One record": "Jeden rekord",
|
||||
steamApiKeyDescription: "Do monitorowania serwera gier Steam potrzebny jest klucz Steam Web-API. Możesz zarejestrować swój klucz API tutaj: ",
|
||||
"Current User": "Aktualny użytkownik",
|
||||
topic: "Temat",
|
||||
topicExplanation: "Temat MQTT do monitorowania",
|
||||
successMessage: "Komunikat o powodzeniu",
|
||||
successMessageExplanation: "Komunikat MQTT, który zostanie uznany za powodzenie",
|
||||
recent: "Ostatnie",
|
||||
Done: "Zrobione",
|
||||
Info: "Info",
|
||||
@@ -344,7 +349,7 @@ export default {
|
||||
Discard: "Odrzuć",
|
||||
Cancel: "Anuluj",
|
||||
"Powered by": "Napędzane przez",
|
||||
shrinkDatabaseDescription: "Uruchom VACUUM na bazie SQLite. Jeżeli twoja baza została stworzona po wersji 1.10.0, to posiada już włączoną opcję AUTO_VACUUM i stosowanie ręcznego oczyszczania nie jest potrzebne.",
|
||||
shrinkDatabaseDescription: "Uruchom VACUUM na bazie SQLite. Jeżeli twoja baza została stworzona po wersji 1.10.0, to ma już włączoną opcję AUTO_VACUUM i stosowanie ręcznego oczyszczania nie jest potrzebne.",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
apiCredentials: "Poświadczenia API",
|
||||
serwersms: "SerwerSMS.pl",
|
||||
@@ -352,14 +357,111 @@ export default {
|
||||
serwersmsAPIPassword: "Hasło API",
|
||||
serwersmsPhoneNumber: "Numer telefonu",
|
||||
serwersmsSenderName: "Nazwa nadawcy (zatwierdzona w panelu klienta)",
|
||||
"stackfield": "Stackfield",
|
||||
stackfield: "Stackfield",
|
||||
Customize: "Dostosuj",
|
||||
"Custom Footer": "Niestandardowa stopka",
|
||||
"Custom CSS": "Niestandardowy CSS",
|
||||
smtpDkimSettings: "Ustawienia DKIM",
|
||||
smtpDkimDesc: "Zapoznaj się z Nodemailer DKIM {0}, aby dowiedzieć się więcej",
|
||||
documentation: "dokumentacja",
|
||||
smtpDkimDomain: "Nazwa domeny",
|
||||
smtpDkimKeySelector: "Selektor klucza",
|
||||
smtpDkimPrivateKey: "Klucz prywatny",
|
||||
smtpDkimHashAlgo: "Algorytm Hashowania (opcjonalne)",
|
||||
smtpDkimHashAlgo: "Algorytm haszujący (opcjonalne)",
|
||||
smtpDkimheaderFieldNames: "Klucze nagłówka do podpisu (opcjonalne)",
|
||||
smtpDkimskipFields: "Klucze nagłówka do pominięcia (opcjonalne)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "Punkt końcowy API",
|
||||
alertaEnvironment: "Środowisko",
|
||||
alertaApiKey: "Klucz API",
|
||||
alertaAlertState: "Alert State",
|
||||
alertaRecoverState: "Recover State",
|
||||
deleteStatusPageMsg: "Jesteś pewien, że chcesz usunąć tę stronę statusów?",
|
||||
Proxies: "Proxy",
|
||||
default: "Domyślny",
|
||||
enabled: "Włączony",
|
||||
setAsDefault: "Ustaw jako domyślny",
|
||||
deleteProxyMsg: "Jesteś pewien, że chcesz usunąć proxy ze wszystkich monitorów?",
|
||||
proxyDescription: "Proxy muszą być przypisane do monitora, aby działały.",
|
||||
enableProxyDescription: "Ten serwer proxy nie będzie miał wpływu na żądania monitorów, dopóki nie zostanie aktywowany. Możesz kontrolować tymczasowe wyłączenie serwera proxy ze wszystkich monitorów za pomocą statusu aktywacji.",
|
||||
setAsDefaultProxyDescription: "Ten serwer proxy będzie domyślnie włączony dla nowych monitorów. Można go jednak wyłączyć osobno dla każdego monitora.",
|
||||
"Certificate Chain": "Łańcuch certyfikatów",
|
||||
Valid: "Ważny",
|
||||
Invalid: "Nieważny",
|
||||
AccessKeyId: "AccessKey ID",
|
||||
SecretAccessKey: "AccessKey Sekret",
|
||||
PhoneNumbers: "Numery telefonów",
|
||||
TemplateCode: "Kod szablonu",
|
||||
SignName: "Podpis",
|
||||
"Sms template must contain parameters: ": "Szablon sms musi posiadać parametry: ",
|
||||
"Bark Endpoint": "Punkt końcowy Bark",
|
||||
WebHookUrl: "WebHookUrl",
|
||||
SecretKey: "Tajny klucz",
|
||||
"For safety, must use secret key": "Ze względów bezpieczeństwa musisz użyć tajnego klucza",
|
||||
"Device Token": "Device Token",
|
||||
Platform: "Platforma",
|
||||
iOS: "iOS",
|
||||
Android: "Android",
|
||||
Huawei: "Huawei",
|
||||
High: "Wysoki",
|
||||
Retry: "Ponów",
|
||||
Topic: "Temat",
|
||||
"WeCom Bot Key": "Klucz bota WeCom",
|
||||
"Setup Proxy": "Skonfiguruj proxy",
|
||||
"Proxy Protocol": "Protokół proxy",
|
||||
"Proxy Server": "Serwer proxy",
|
||||
"Proxy server has authentication": "Serwer proxy ma autoryzację",
|
||||
User: "Użytkownik",
|
||||
Installed: "Zainstalowany",
|
||||
"Not installed": "Nie zainstalowany",
|
||||
Running: "Działa",
|
||||
"Not running": "Nie działa",
|
||||
"Remove Token": "Usuń token",
|
||||
Start: "Start",
|
||||
Stop: "Stop",
|
||||
"Uptime Kuma": "Uptime Kuma",
|
||||
"Add New Status Page": "Dodaj nową stronę statusów",
|
||||
Slug: "Symbol",
|
||||
"Accept characters:": "Dozwolone znaki:",
|
||||
startOrEndWithOnly: "Zaczynające się i kończące wyłącznie {0} znakami",
|
||||
"No consecutive dashes": "Bez powtarzających się myślników",
|
||||
Next: "Dalej",
|
||||
"The slug is already taken. Please choose another slug.": "Ten symbol jest już zajęty. Proszę, wybierz inny.",
|
||||
"No Proxy": "Bez proxy",
|
||||
"HTTP Basic Auth": "Podstawowa autoryzacja HTTP",
|
||||
"New Status Page": "Nowa strona statusu",
|
||||
"Page Not Found": "Strona nie została znaleziona",
|
||||
"Reverse Proxy": "Odwrotne Proxy",
|
||||
Backup: "Backup",
|
||||
About: "O skrypcie",
|
||||
wayToGetCloudflaredURL: "(Pobierz cloudflared z {0})",
|
||||
cloudflareWebsite: "Strona Cloudflare",
|
||||
"Message:": "Wiadomość:",
|
||||
"Don't know how to get the token? Please read the guide:": "Nie wiesz jak uzyksać token? Przeczytaj proszę poradnik:",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Bieżące połączenie może zostać utracone, jeśli aktualnie łączysz się przez tunel Cloudflare. Czy na pewno chcesz to przerwać? Wpisz swoje aktualne hasło, aby je potwierdzić.",
|
||||
"Other Software": "Inne oprogramowanie",
|
||||
"For example: nginx, Apache and Traefik.": "Na przykład: nginx, Apache i Traefik.",
|
||||
"Please read": "Przeczytaj proszę",
|
||||
"Subject:": "Temat:",
|
||||
"Valid To:": "Ważdny do:",
|
||||
"Days Remaining:": "Pozostało dni:",
|
||||
"Issuer:": "Wydawca:",
|
||||
"Fingerprint:": "Odcisk palca:",
|
||||
"No status pages": "Brak stron statusów",
|
||||
"Domain Name Expiry Notification": "Powiadomienie o wygasaniu domeny",
|
||||
Proxy: "Proxy",
|
||||
"Date Created": "Data stworzenia",
|
||||
onebotHttpAddress: "Adres HTTP OneBot",
|
||||
onebotMessageType: "Rodzaj wiadomości OneBot",
|
||||
onebotGroupMessage: "Grupowa",
|
||||
onebotPrivateMessage: "Prywatna",
|
||||
onebotUserOrGroupId: "ID Grupy/Użytkownika",
|
||||
onebotSafetyTips: "Ze względów bezpieczeństwa musisz ustawić token dostępu",
|
||||
"PushDeer Key": "Klucz PushDeer",
|
||||
"Footer Text": "Treść stopki",
|
||||
"Show Powered By": "Pokaż co napędza stronę",
|
||||
"Domain Names": "Domeny",
|
||||
signedInDisp: "Zalogowany jako {0}",
|
||||
signedInDispDisabled: "Autoryzacja wyłączona.",
|
||||
};
|
||||
|
@@ -381,7 +381,7 @@ export default {
|
||||
smtpDkimPrivateKey: "Приватный ключ",
|
||||
smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
|
||||
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
|
||||
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)",
|
||||
smtpDkimskipFields: "Заголовок ключей не для подписи (опционально)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "Конечная точка API",
|
||||
|
@@ -239,6 +239,7 @@ export default {
|
||||
"rocket.chat": "Rocket.chat",
|
||||
pushover: "Pushover",
|
||||
pushy: "Pushy",
|
||||
PushByTechulus: "Push by Techulus",
|
||||
octopush: "Octopush",
|
||||
promosms: "PromoSMS",
|
||||
clicksendsms: "ClickSend SMS",
|
||||
@@ -308,6 +309,10 @@ export default {
|
||||
"One record": "One record",
|
||||
steamApiKeyDescription: "Để theo dõi các Steam Game Server bạn cần một Steam Web-API key. Bạn có thể đăng ký API key tại đây: ",
|
||||
"Current User": "User hiện tại",
|
||||
topic: "Topic",
|
||||
topicExplanation: "MQTT topic to monitor",
|
||||
successMessage: "Success Message",
|
||||
successMessageExplanation: "MQTT message that will be considered as success",
|
||||
recent: "Gần đây",
|
||||
Done: "Hoàn thành",
|
||||
Info: "Thông tin",
|
||||
@@ -353,6 +358,9 @@ export default {
|
||||
serwersmsPhoneNumber: "Số điện thoại",
|
||||
serwersmsSenderName: "Tên người gửi SMS (Đã đăng ký qua portal)",
|
||||
"stackfield": "Stackfield",
|
||||
Customize: "Customize",
|
||||
"Custom Footer": "Custom Footer",
|
||||
"Custom CSS": "Custom CSS",
|
||||
smtpDkimSettings: "Cài đặt xác thực Email(DKIM)",
|
||||
smtpDkimDesc: "Xem hướng dẫn tại {0}.",
|
||||
documentation: "Nodemailer DKIM",
|
||||
@@ -362,4 +370,98 @@ export default {
|
||||
smtpDkimHashAlgo: "Hash Algorithm (Tuỳ chọn)",
|
||||
smtpDkimheaderFieldNames: "Header Keys to sign (Tuỳ chọn)",
|
||||
smtpDkimskipFields: "Header Keys not to sign (Tuỳ chọn)",
|
||||
gorush: "Gorush",
|
||||
alerta: "Alerta",
|
||||
alertaApiEndpoint: "API Endpoint",
|
||||
alertaEnvironment: "Environment",
|
||||
alertaApiKey: "API Key",
|
||||
alertaAlertState: "Alert State",
|
||||
alertaRecoverState: "Recover State",
|
||||
deleteStatusPageMsg: "Bạn có chắc chắn muốn xoá trang status này?",
|
||||
Proxies: "Proxies",
|
||||
default: "Mặc định",
|
||||
enabled: "Enabled",
|
||||
setAsDefault: "Set As Default",
|
||||
deleteProxyMsg: "Bạn muốn xoá proxy này cho tất cả monitors?",
|
||||
proxyDescription: "Proxies must be assigned to a monitor to function.",
|
||||
enableProxyDescription: "Proxy này chưa ảnh hưởng tới monitor requests cho tới khi được activated. Bạn có thể tạm thời tắt proxy cho tất cả monitors bằng trạng thái activation.",
|
||||
setAsDefaultProxyDescription: "Proxy này sẽ bật mặc định cho tất cả monitors mới. Bạn có thể tắt riêng lẻ proxy trên mỗi monitor.",
|
||||
"Certificate Chain": "Certificate Chain",
|
||||
Valid: "Hợp lệ",
|
||||
Invalid: "Không hợp lệ",
|
||||
AccessKeyId: "AccessKey ID",
|
||||
SecretAccessKey: "AccessKey Secret",
|
||||
PhoneNumbers: "PhoneNumbers",
|
||||
TemplateCode: "TemplateCode",
|
||||
SignName: "SignName",
|
||||
"Sms template must contain parameters: ": "Sms template must contain parameters: ",
|
||||
"Bark Endpoint": "Bark Endpoint",
|
||||
WebHookUrl: "WebHookUrl",
|
||||
SecretKey: "SecretKey",
|
||||
"For safety, must use secret key": "Để an toàn, hãy dùng secret key",
|
||||
"Device Token": "Device Token",
|
||||
Platform: "Platform",
|
||||
iOS: "iOS",
|
||||
Android: "Android",
|
||||
Huawei: "Huawei",
|
||||
High: "High",
|
||||
Retry: "Retry",
|
||||
Topic: "Topic",
|
||||
"WeCom Bot Key": "WeCom Bot Key",
|
||||
"Setup Proxy": "Setup Proxy",
|
||||
"Proxy Protocol": "Proxy Protocol",
|
||||
"Proxy Server": "Proxy Server",
|
||||
"Proxy server has authentication": "Proxy server has authentication",
|
||||
User: "User",
|
||||
Installed: "Installed",
|
||||
"Not installed": "Not installed",
|
||||
Running: "Running",
|
||||
"Not running": "Not running",
|
||||
"Remove Token": "Remove Token",
|
||||
Start: "Start",
|
||||
Stop: "Stop",
|
||||
"Uptime Kuma": "Uptime Kuma",
|
||||
"Add New Status Page": "Thêm mới Status Page",
|
||||
Slug: "Slug",
|
||||
"Accept characters:": "Accept characters:",
|
||||
startOrEndWithOnly: "Start or end with {0} only",
|
||||
"No consecutive dashes": "No consecutive dashes",
|
||||
Next: "Next",
|
||||
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
|
||||
"No Proxy": "No Proxy",
|
||||
"HTTP Basic Auth": "HTTP Basic Auth",
|
||||
"New Status Page": "New Status Page",
|
||||
"Page Not Found": "Page Not Found",
|
||||
"Reverse Proxy": "Reverse Proxy",
|
||||
Backup: "Backup",
|
||||
About: "About",
|
||||
wayToGetCloudflaredURL: "(Download cloudflared from {0})",
|
||||
cloudflareWebsite: "Cloudflare Website",
|
||||
"Message:": "Message:",
|
||||
"Don't know how to get the token? Please read the guide:": "Chưa biết cách lấy token? Xem hướng dẫn tại:",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Nếu bạn đang dùng Cloudflare Tunnel, kết nối hiện tại có thể đang bị mất. Bạn có muốn dừng lại? Nhập lại password để xác nhận.",
|
||||
"Other Software": "Phần mềm khác",
|
||||
"For example: nginx, Apache and Traefik.": "Ví dụ: Nginx, Apache hay Traefik.",
|
||||
"Please read": "Hãy xem qua",
|
||||
"Subject:": "Subject:",
|
||||
"Valid To:": "Valid To:",
|
||||
"Days Remaining:": "Số ngày còn lại:",
|
||||
"Issuer:": "Issuer:",
|
||||
"Fingerprint:": "Fingerprint:",
|
||||
"No status pages": "No status pages",
|
||||
"Domain Name Expiry Notification": "Cảnh báo hạn hạn Domain Name",
|
||||
Proxy: "Proxy",
|
||||
"Date Created": "Ngày khởi tạo",
|
||||
onebotHttpAddress: "OneBot HTTP Address",
|
||||
onebotMessageType: "OneBot Message Type",
|
||||
onebotGroupMessage: "Group",
|
||||
onebotPrivateMessage: "Private",
|
||||
onebotUserOrGroupId: "Group/User ID",
|
||||
onebotSafetyTips: "Để đảm bảo an toàn, hãy thiết lập access token",
|
||||
"PushDeer Key": "PushDeer Key",
|
||||
"Footer Text": "Footer Text",
|
||||
"Show Powered By": "Show Powered By",
|
||||
"Domain Names": "Domain Names",
|
||||
signedInDisp: "Signed in as {0}",
|
||||
signedInDispDisabled: "Auth Disabled.",
|
||||
};
|
||||
|
@@ -88,7 +88,7 @@ export default {
|
||||
Dark: "黑暗",
|
||||
Auto: "自动",
|
||||
"Theme - Heartbeat Bar": "主题 - 心跳栏",
|
||||
Normal: "正常", // 此处还供 Gorush 的通知优先级功能使用,不应翻译为“正常显示”
|
||||
Normal: "正常",
|
||||
Bottom: "靠下",
|
||||
None: "不显示",
|
||||
Timezone: "时区",
|
||||
@@ -398,11 +398,9 @@ export default {
|
||||
Invalid: "无效",
|
||||
AccessKeyId: "AccessKey ID",
|
||||
SecretAccessKey: "AccessKey Secret",
|
||||
/* 以下为阿里云短信服务 API Dysms#SendSms 的参数 */
|
||||
PhoneNumbers: "PhoneNumbers",
|
||||
TemplateCode: "TemplateCode",
|
||||
SignName: "SignName",
|
||||
/* 以上为阿里云短信服务 API Dysms#SendSms 的参数 */
|
||||
"Bark Endpoint": "Bark 接入点",
|
||||
"Device Token": "Apple Device Token",
|
||||
Platform: "平台",
|
||||
@@ -441,7 +439,7 @@ export default {
|
||||
"No Proxy": "无代理",
|
||||
"HTTP Basic Auth": "HTTP 基础身份验证",
|
||||
"New Status Page": "新的状态页",
|
||||
"Page Not Found": "状态页未找到",
|
||||
"Page Not Found": "未找到该页面",
|
||||
"Reverse Proxy": "反向代理",
|
||||
"Subject:": "颁发给:",
|
||||
"Valid To:": "有效期至:",
|
||||
@@ -469,4 +467,57 @@ export default {
|
||||
"Footer Text": "底部自定义文本",
|
||||
"Show Powered By": "显示 Powered By",
|
||||
"Domain Names": "域名",
|
||||
"Certificate Expiry Notification": "证书到期时通知",
|
||||
"API Username": "API 凭证 Username",
|
||||
"API Key": "API 凭证 Key",
|
||||
"Recipient Number": "收件人手机号码",
|
||||
"From Name/Number": "发件人名称/手机号码",
|
||||
"Leave blank to use a shared sender number.": "留空以使用平台共享的发件人手机号码",
|
||||
"Octopush API Version": "Octopush API 版本",
|
||||
"Legacy Octopush-DM": "旧版本 Octopush-DM",
|
||||
endpoint: "接入点",
|
||||
octopushAPIKey: "控制台 HTTP API credentials 里的 \"API key\"",
|
||||
octopushLogin: "控制台 HTTP API credentials 里的 \"Login\"",
|
||||
promosmsLogin: "API 登录名",
|
||||
promosmsPassword: "API 密码",
|
||||
"pushoversounds pushover": "Pushover(默认)",
|
||||
"pushoversounds bike": "Bike",
|
||||
"pushoversounds bugle": "Bugle",
|
||||
"pushoversounds cashregister": "Cash Register",
|
||||
"pushoversounds classical": "Classical",
|
||||
"pushoversounds cosmic": "Cosmic",
|
||||
"pushoversounds falling": "Falling",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "Incoming",
|
||||
"pushoversounds intermission": "Intermission",
|
||||
"pushoversounds magic": "Magic",
|
||||
"pushoversounds mechanical": "Mechanical",
|
||||
"pushoversounds pianobar": "Piano Bar",
|
||||
"pushoversounds siren": "Siren",
|
||||
"pushoversounds spacealarm": "Space Alarm",
|
||||
"pushoversounds tugboat": "Tug Boat",
|
||||
"pushoversounds alien": "Alien Alarm(长铃声)",
|
||||
"pushoversounds climb": "Climb(长铃声)",
|
||||
"pushoversounds persistent": "Persistent(长铃声)",
|
||||
"pushoversounds echo": "Pushover Echo(长铃声)",
|
||||
"pushoversounds updown": "Up Down(长铃声)",
|
||||
"pushoversounds vibrate": "仅震动",
|
||||
"pushoversounds none": "无(禁音)",
|
||||
pushyAPIKey: "API 密钥",
|
||||
pushyToken: "设备 Token",
|
||||
"Show update if available": "有更新时通知",
|
||||
"Also check beta release": "一并检查 Beta 版更新",
|
||||
"Using a Reverse Proxy?": "正在使用反向代理?",
|
||||
"Check how to config it for WebSocket": "查看如何将反向代理与 WebSocket 一起使用",
|
||||
"Steam Game Server": "Steam 游戏服务器",
|
||||
"Most likely causes:": "最可能的原因:",
|
||||
"The resource is no longer available.": "您所请求的资源已不再可用;",
|
||||
"There might be a typing error in the address.": "您输入的地址可能有误。",
|
||||
"What you can try:": "您可以尝试以下操作:",
|
||||
"Retype the address.": "重新输入地址;",
|
||||
"Go back to the previous page.": "返回到上一页面。",
|
||||
"Coming Soon": "即将推出",
|
||||
wayToGetClickSendSMSToken: "您可以从 {0} 获取 API 凭证 Username 和 凭证 Key。",
|
||||
signedInDisp: "当前用户: {0}",
|
||||
signedInDispDisabled: "已禁用身份验证",
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<div class="container-fluid">
|
||||
{{ $root.connectionErrorMsg }}
|
||||
<div v-if="$root.showReverseProxyGuide">
|
||||
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
|
||||
{{ $t("Using a Reverse Proxy?") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">{{ $t("Check how to config it for WebSocket") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
</li>
|
||||
<li v-if="$root.loggedIn" class="nav-item">
|
||||
<div class="dropdown dropdown-profile-pic">
|
||||
<div type="button" class="nav-link" data-bs-toggle="dropdown">
|
||||
<div class="nav-link" data-bs-toggle="dropdown">
|
||||
<div class="profile-pic">{{ $root.usernameFirstChar }}</div>
|
||||
<font-awesome-icon icon="angle-down" />
|
||||
</div>
|
||||
@@ -71,7 +71,7 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<router-view v-if="$root.loggedIn || forceShowContent" />
|
||||
<router-view v-if="$root.loggedIn" />
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
</main>
|
||||
|
||||
|
@@ -30,7 +30,7 @@
|
||||
Push
|
||||
</option>
|
||||
<option value="steam">
|
||||
Steam Game Server
|
||||
{{ $t("Steam Game Server") }}
|
||||
</option>
|
||||
<option value="mqtt">
|
||||
MQTT
|
||||
@@ -56,7 +56,7 @@
|
||||
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
|
||||
<div class="form-text">
|
||||
{{ $t("needPushEvery", [monitor.interval]) }}<br />
|
||||
{{ $t("pushOptionalParams", ["msg, ping"]) }}
|
||||
{{ $t("pushOptionalParams", ["status, msg, ping"]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="expiry-notification">
|
||||
{{ $t("Domain Name Expiry Notification") }}
|
||||
{{ $t("Certificate Expiry Notification") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
</div>
|
||||
@@ -361,13 +361,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { useToast } from "vue-toastification";
|
||||
import CopyableInput from "../components/CopyableInput.vue";
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||
import TagsManager from "../components/TagsManager.vue";
|
||||
import CopyableInput from "../components/CopyableInput.vue";
|
||||
|
||||
import { useToast } from "vue-toastification";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { genSecret, isDev } from "../util.ts";
|
||||
|
||||
const toast = useToast();
|
||||
@@ -422,7 +421,7 @@ export default {
|
||||
},
|
||||
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||
},
|
||||
|
||||
bodyPlaceholder() {
|
||||
@@ -540,7 +539,7 @@ export default {
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.$root.notificationList.length; i++) {
|
||||
if (this.$root.notificationList[i].isDefault == true) {
|
||||
if (this.$root.notificationList[i].isDefault === true) {
|
||||
this.monitor.notificationIDList[this.$root.notificationList[i].id] = true;
|
||||
}
|
||||
}
|
||||
|
@@ -22,16 +22,16 @@
|
||||
</div>
|
||||
|
||||
<div class="guide">
|
||||
Most likely causes:
|
||||
{{ $t("Most likely causes:") }}
|
||||
<ul>
|
||||
<li>The resource is no longer available.</li>
|
||||
<li>There might be a typing error in the address.</li>
|
||||
<li>{{ $t("The resource is no longer available.") }}</li>
|
||||
<li>{{ $t("There might be a typing error in the address.") }}</li>
|
||||
</ul>
|
||||
|
||||
What you can try:<br />
|
||||
{{ $t("What you can try:") }}<br />
|
||||
<ul>
|
||||
<li>Retype the address.</li>
|
||||
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li>
|
||||
<li>{{ $t("Retype the address.") }}</li>
|
||||
<li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="false" class="my-3">
|
||||
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
|
||||
<label for="password" class="form-label">{{ $t("Password") }} <sup>{{ $t("Coming Soon") }}</sup></label>
|
||||
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
|
||||
</div>
|
||||
|
||||
@@ -104,15 +104,16 @@
|
||||
|
||||
<!-- Uploader -->
|
||||
<!-- url="/api/status-page/upload-logo" -->
|
||||
<ImageCropUpload v-model="showImageCropUpload"
|
||||
field="img"
|
||||
:width="128"
|
||||
:height="128"
|
||||
:langType="$i18n.locale"
|
||||
img-format="png"
|
||||
:noCircle="true"
|
||||
:noSquare="false"
|
||||
@crop-success="cropSuccess"
|
||||
<ImageCropUpload
|
||||
v-model="showImageCropUpload"
|
||||
field="img"
|
||||
:width="128"
|
||||
:height="128"
|
||||
:langType="$i18n.locale"
|
||||
img-format="png"
|
||||
:noCircle="true"
|
||||
:noSquare="false"
|
||||
@crop-success="cropSuccess"
|
||||
/>
|
||||
|
||||
<!-- Title -->
|
||||
@@ -281,22 +282,21 @@
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import ImageCropUpload from "vue-image-crop-upload";
|
||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
import Favico from "favico.js";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
// import Prism Editor
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
|
||||
|
||||
// import highlighting library (you can use any library you want just return html string)
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import "prismjs/components/prism-css";
|
||||
import "prismjs/themes/prism-tomorrow.css"; // import syntax highlighting styles
|
||||
import ImageCropUpload from "vue-image-crop-upload";
|
||||
// import Prism Editor
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
|
||||
import { useToast } from "vue-toastification";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
|
@@ -102,7 +102,7 @@ class Logger {
|
||||
}
|
||||
else if (level === "DEBUG") {
|
||||
if (exports.isDev) {
|
||||
console.debug(formattedMessage);
|
||||
console.log(formattedMessage);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@@ -113,7 +113,7 @@ class Logger {
|
||||
console.error(formattedMessage);
|
||||
} else if (level === "DEBUG") {
|
||||
if (isDev) {
|
||||
console.debug(formattedMessage);
|
||||
console.log(formattedMessage);
|
||||
}
|
||||
} else {
|
||||
console.log(formattedMessage);
|
||||
|
@@ -1,5 +1,9 @@
|
||||
const { genSecret } = require("../src/util");
|
||||
const { genSecret, DOWN } = require("../src/util");
|
||||
const utilServerRewire = require("../server/util-server");
|
||||
const Discord = require("../server/notification-providers/discord");
|
||||
const axios = require("axios");
|
||||
|
||||
jest.mock("axios");
|
||||
|
||||
describe("Test parseCertificateInfo", () => {
|
||||
it("should handle undefined", async () => {
|
||||
@@ -164,3 +168,86 @@ describe("Test reset-password", () => {
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
describe("Test Discord Notification Provider", () => {
|
||||
const sendNotification = async (hostname, port, type) => {
|
||||
const discordProvider = new Discord();
|
||||
|
||||
axios.post.mockResolvedValue({});
|
||||
|
||||
await discordProvider.send(
|
||||
{
|
||||
discordUsername: "Uptime Kuma",
|
||||
discordWebhookUrl: "https://discord.com",
|
||||
},
|
||||
"test message",
|
||||
{
|
||||
type,
|
||||
hostname,
|
||||
port,
|
||||
},
|
||||
{
|
||||
status: DOWN,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
it("should send hostname for dns monitors", async () => {
|
||||
const hostname = "discord.com";
|
||||
await sendNotification(hostname, null, "dns");
|
||||
|
||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
|
||||
hostname
|
||||
);
|
||||
});
|
||||
|
||||
it("should send hostname for ping monitors", async () => {
|
||||
const hostname = "discord.com";
|
||||
await sendNotification(hostname, null, "ping");
|
||||
|
||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
|
||||
hostname
|
||||
);
|
||||
});
|
||||
|
||||
it("should send hostname for port monitors", async () => {
|
||||
const hostname = "discord.com";
|
||||
const port = 1337;
|
||||
await sendNotification(hostname, port, "port");
|
||||
|
||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
|
||||
`${hostname}:${port}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should send hostname for steam monitors", async () => {
|
||||
const hostname = "discord.com";
|
||||
const port = 1337;
|
||||
await sendNotification(hostname, port, "steam");
|
||||
|
||||
expect(axios.post.mock.lastCall[1].embeds[0].fields[1].value).toBe(
|
||||
`${hostname}:${port}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("The function filterAndJoin", () => {
|
||||
it("should join and array of strings to one string", () => {
|
||||
const result = utilServerRewire.filterAndJoin(["one", "two", "three"]);
|
||||
expect(result).toBe("onetwothree");
|
||||
});
|
||||
|
||||
it("should join strings using a given connector", () => {
|
||||
const result = utilServerRewire.filterAndJoin(["one", "two", "three"], "-");
|
||||
expect(result).toBe("one-two-three");
|
||||
});
|
||||
|
||||
it("should filter null, undefined and empty strings before joining", () => {
|
||||
const result = utilServerRewire.filterAndJoin([undefined, "", "three"], "--");
|
||||
expect(result).toBe("three");
|
||||
});
|
||||
|
||||
it("should return an empty string if all parts are filtered out", () => {
|
||||
const result = utilServerRewire.filterAndJoin([undefined, "", ""], "--");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
@@ -284,6 +284,11 @@ describe("Init", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test login
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
*/
|
||||
async function login(username, password) {
|
||||
await input(page, "#floatingInput", username);
|
||||
await input(page, "#floatingPassword", password);
|
||||
@@ -291,6 +296,13 @@ async function login(username, password) {
|
||||
await sleep(5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an element on the page
|
||||
* @param {Page} page Puppeteer page instance
|
||||
* @param {string} selector
|
||||
* @param {number} elementIndex
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async function click(page, selector, elementIndex = 0) {
|
||||
await page.waitForSelector(selector, {
|
||||
timeout: 5000,
|
||||
@@ -300,6 +312,12 @@ async function click(page, selector, elementIndex = 0) {
|
||||
}, selector, elementIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input text into selected field
|
||||
* @param {Page} page Puppeteer page instance
|
||||
* @param {string} selector
|
||||
* @param {string} text Text to input
|
||||
*/
|
||||
async function input(page, selector, text) {
|
||||
await page.waitForSelector(selector, {
|
||||
timeout: 5000,
|
||||
|
Reference in New Issue
Block a user