Compare commits

..

168 Commits
1.0.6 ... 1.0.9

Author SHA1 Message Date
LouisLam
807db8a2d8 update to 1.0.9 2021-08-04 01:04:13 +08:00
LouisLam
d707eba046 fix disable auth 2021-08-04 01:03:40 +08:00
Philipp Dormann
e34a8e2e4a FEAT: PUSHY Notifier (#154)
FEAT: PUSHY Notifier (#154)
2021-08-03 23:14:27 +08:00
Louis Lam
6bd9d85a9a Merge pull request #150 from chakflying/created_date
Fix: [DB] Add default for created_date in monitor
2021-08-03 22:58:56 +08:00
LouisLam
f2de6299f6 update .dockerignore 2021-08-03 20:42:32 +08:00
LouisLam
a28d6eafae remove apprise --version from dockerfile 2021-08-03 20:35:41 +08:00
LouisLam
fce0edebc9 Merge remote-tracking branch 'origin/master' 2021-08-03 19:56:23 +08:00
LouisLam
48a4ced9a5 update to 1.0.8 2021-08-03 19:36:21 +08:00
Nelson Chan
221aad55de Chore: Add new line at EOF 2021-08-03 17:46:09 +08:00
Nelson Chan
377d475e05 Fix: Add now columns 2021-08-03 17:43:39 +08:00
Nelson Chan
0c3c59df4e Fix: [DB] Add default for created_date in monitor 2021-08-03 17:42:57 +08:00
Louis Lam
eba996b0f2 Delete codeql-analysis.yml 2021-08-03 15:17:48 +08:00
LouisLam
4d71e03039 improve #39 2021-08-03 15:14:26 +08:00
LouisLam
2740f096c0 Merge remote-tracking branch 'origin/master' 2021-08-03 13:07:37 +08:00
LouisLam
8ebaca4c5c improve disableAuth handling 2021-08-03 13:07:20 +08:00
Louis Lam
fceb594442 Merge pull request #145 from chakflying/patch-3
Fix: Increase width of status pill
2021-08-03 11:04:30 +08:00
Nelson Chan
5b9d3357aa Fix: Increase width of status pill 2021-08-03 10:57:56 +08:00
LouisLam
5689b30985 Merge remote-tracking branch 'origin/master' 2021-08-03 00:08:59 +08:00
LouisLam
44c8ca9da8 requires empty username/password if set disableAuth for basic auth 2021-08-03 00:08:46 +08:00
Louis Lam
6f044de6e6 Update README.md 2021-08-01 11:11:04 +08:00
Louis Lam
7877adf7a3 Merge pull request #137 from louislam/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2021-08-01 00:36:05 +08:00
Louis Lam
f0e5e9f463 Create CODE_OF_CONDUCT.md 2021-08-01 00:35:47 +08:00
Louis Lam
0263cfa7e4 Create CONTRIBUTING.md
move wiki to CONTRIBUTING.md
2021-08-01 00:29:30 +08:00
Louis Lam
8b733592cb Update README.md 2021-08-01 00:23:40 +08:00
Louis Lam
71fa55c218 Update README.md 2021-08-01 00:19:04 +08:00
Louis Lam
ee071e41f5 Update README.md 2021-08-01 00:15:33 +08:00
LouisLam
6f868c9ec3 implement no auth 2021-07-31 23:41:24 +08:00
LouisLam
33d7f8645a json format for setting value 2021-07-31 22:02:30 +08:00
LouisLam
c6a66fad79 add setting for disable auth 2021-07-31 21:57:58 +08:00
LouisLam
9f0be5f531 improve the connection error msg 2021-07-31 21:13:32 +08:00
LouisLam
7f42888546 fix eslint for vue (https://github.com/louislam/uptime-kuma/pull/121#issuecomment-889729900) 2021-07-31 20:42:51 +08:00
LouisLam
642a711bcd Confirm Dialog: allow changing the button text 2021-07-31 18:58:12 +08:00
LouisLam
659d83b13c turn off vue/html-self-closing, empty div should be allowed 2021-07-31 18:31:17 +08:00
LouisLam
4b93900866 fix eslint for vue (https://github.com/louislam/uptime-kuma/pull/121#issuecomment-889729900) 2021-07-31 14:46:57 +08:00
Louis Lam
204624bfe9 Merge pull request #133 from NiNiyas/lunasea-support
Adds support for LunaSea notifications
2021-07-31 14:02:29 +08:00
LouisLam
b7fbc2c0e6 add LinaSea option in select box 2021-07-31 13:37:42 +08:00
LouisLam
2ebd79d037 run eslint for lunasea change 2021-07-31 13:35:18 +08:00
Niyas
3f84e5e8ab Update notification.js 2021-07-31 10:51:13 +05:30
Niyas
ab1fe2e2d1 LunaSea Support 2021-07-31 10:33:20 +05:30
Niyas
67a4e949a2 LunaSea Support 2021-07-31 10:31:41 +05:30
Louis Lam
15ee853fac Merge pull request #132 from sashkab/dockerfile-fix
Simplify apprise installation
2021-07-31 12:45:09 +08:00
LouisLam
d58be56cb9 remove "pip3 cache purge" that causes error 2021-07-31 12:21:39 +08:00
Aleks Bunin
00cc140acd Simplify apprise instalation 2021-07-30 22:32:59 -04:00
LouisLam
63f0a36811 implement upside down mode and ignore tls error 2021-07-31 00:01:04 +08:00
LouisLam
06377af7e5 turn off object-curly-newline, it makes const { a, b, c, d } = require(...) ugly 2021-07-30 22:11:14 +08:00
LouisLam
60aa67892d store ignoreTls and upsideDown into db 2021-07-30 19:18:26 +08:00
LouisLam
17b58eac9a Merge remote-tracking branch 'origin/master' 2021-07-30 15:45:18 +08:00
Louis Lam
2b3a48995b Merge pull request #121 from chakflying/patch-1
Fix: Update ESLint to handle class static member
2021-07-30 15:21:14 +08:00
LouisLam
e032072900 eslint: allow while (true) 2021-07-30 15:13:51 +08:00
LouisLam
bcf2a319c2 update readme 2021-07-30 15:04:52 +08:00
Nelson Chan
cdaa0a54a4 Fix: use new version of babel-eslint-parser 2021-07-30 12:35:33 +08:00
Nelson Chan
47b19ea2f2 ESLint: fix file 2021-07-30 12:35:02 +08:00
Nelson Chan
1006b37a67 Fix: Add fix for babel-eslist 2021-07-30 12:33:01 +08:00
Nelson Chan
b91e9ddb7a Fix: Add babel-eslint 2021-07-30 12:33:01 +08:00
Nelson Chan
be22fcb87d Fix: Bump ES version in ESlint config 2021-07-30 12:33:00 +08:00
LouisLam
5a053e5875 parse the port to int 2021-07-30 11:33:44 +08:00
LouisLam
081abcb6a1 add util.ts for sharing common functions between frontend and backend 2021-07-30 11:23:04 +08:00
LouisLam
71af902a4e add fields to EditMonitor.vue 2021-07-30 01:09:14 +08:00
LouisLam
4b86c84c36 fix icon for "Resume" 2021-07-30 01:02:41 +08:00
LouisLam
f9a10d1672 add back maxretries field 2021-07-30 00:13:48 +08:00
LouisLam
e6915d8964 unexpected space add to router-link due vue/singleline-html-element-content-newline, set it to off 2021-07-29 01:01:55 +08:00
LouisLam
063697c20a set the port by env.PORT, specific node version in package.json 2021-07-29 00:52:41 +08:00
LouisLam
435e4faef3 test heroku deployment 2021-07-29 00:38:52 +08:00
LouisLam
1425d0e91a test heroku deployment 2021-07-29 00:36:35 +08:00
LouisLam
7dbec90c95 cache index.html and fix basic auth applied to all routes 2021-07-28 23:40:50 +08:00
LouisLam
53a90347ca update database schema, add upside_down and ignore_tls 2021-07-28 23:26:27 +08:00
LouisLam
133c7230bc fix resize problem 2021-07-28 23:03:37 +08:00
LouisLam
3666ebb931 change no-unused-vars from error to warn 2021-07-28 20:52:49 +08:00
LouisLam
6bce270f42 cleanup code 2021-07-28 20:35:55 +08:00
LouisLam
4a9690437f Merge branch 'eslint_stylelint'
# Conflicts:
#	server/server.js
2021-07-28 20:20:10 +08:00
Louis Lam
c8c2300483 Merge pull request #120 from chakflying/patch-1
Fix: passwordHash is not imported
2021-07-28 19:34:01 +08:00
Nelson Chan
ac0f418294 Fix: passwordHash is not imported 2021-07-28 10:58:36 +08:00
Adam Stachowicz
d54bc866b4 Fix block-no-empty error from Stylelint 2021-07-27 22:23:46 +02:00
Adam Stachowicz
be1fc0c2b6 Missing this part 2 2021-07-27 20:03:53 +02:00
Adam Stachowicz
d97091af51 Missing this 2021-07-27 20:02:20 +02:00
Adam Stachowicz
4c8fdd07d9 Manual fixes 2021-07-27 19:53:59 +02:00
Adam Stachowicz
9648d700d7 Autofix on save 2021-07-27 19:47:13 +02:00
Adam Stachowicz
8331e795e7 Merge branch 'master' into eslint_stylelint 2021-07-27 19:37:07 +02:00
Adam Stachowicz
3c6af6d3f4 Add ESLint and StyleLint 2021-07-27 19:33:44 +02:00
LouisLam
209fa83cff Add Basic Auth for /metrics 2021-07-28 00:52:31 +08:00
LouisLam
cc6f1d7487 Merge branch 'feature/add_prometheus_metrics' 2021-07-27 23:21:37 +08:00
LouisLam
36436ed4ef Move all Prometheus guides to wiki 2021-07-27 23:14:13 +08:00
LouisLam
934b797623 Merge branch 'master' into feature/add_prometheus_metrics
# Conflicts:
#	server/model/monitor.js
2021-07-27 23:13:03 +08:00
Louis Lam
0f0a6299c0 Create codeql-analysis.yml 2021-07-27 20:25:59 +08:00
Louis Lam
e6ca105600 Update ask-for-help.md 2021-07-27 18:18:38 +08:00
Louis Lam
ef45aedda5 Delete help.md 2021-07-27 18:18:27 +08:00
Louis Lam
7edd79a74d Update issue templates 2021-07-27 18:06:26 +08:00
Louis Lam
fade240c7f Delete --please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md 2021-07-27 18:05:11 +08:00
Louis Lam
46337ec348 Update issue templates 2021-07-27 18:04:55 +08:00
LouisLam
cafd2c7388 add vue-fontawesone 2021-07-27 16:52:44 +08:00
LouisLam
4d7c2d329b Update to 1.0.7 2021-07-27 13:47:15 +08:00
Louis Lam
1982e2f8b8 Update README.md 2021-07-27 00:53:26 +08:00
LouisLam
2819094377 improve the page load performance 2021-07-26 23:26:47 +08:00
LouisLam
06c4523ce3 update the latest db version to 3 2021-07-26 23:05:04 +08:00
LouisLam
ac3732f6cc Merge remote-tracking branch 'chakflying/tls-expiry' into tls-expiry
# Conflicts:
#	server/model/monitor.js
#	src/pages/Details.vue
2021-07-26 22:56:17 +08:00
LouisLam
bf3e9dccd2 improve the ui of cert info 2021-07-26 22:53:07 +08:00
LouisLam
5b18a6a518 Merge branch 'master' into tls-expiry
# Conflicts:
#	server/model/monitor.js
2021-07-26 20:35:50 +08:00
LouisLam
caec933186 prevent unexpected error throw from checkCertificate interrupt the beat 2021-07-26 12:25:44 +08:00
Nelson Chan
51ac7a58dc Fix: Fix incorrect error handling 2021-07-26 12:24:13 +08:00
Nelson Chan
db26b7d123 Fix: Fix no certificate caused by session reuse 2021-07-26 12:24:13 +08:00
Nelson Chan
7b8459c73a Fix: use Optional chaining 2021-07-26 12:24:12 +08:00
Nelson Chan
d0c63ebe3e Feat: Add database storage for TLS info 2021-07-26 12:24:12 +08:00
Nelson Chan
803f0d6219 Feat: Add Barebones certificate info display 2021-07-26 12:24:06 +08:00
LouisLam
d556509d07 戈mprove the readibility of important condition 2021-07-24 11:42:14 +08:00
Louis Lam
3cc4955cad Merge pull request #105 from rezzorix/master
Apple touch icon 192px with preserved transparency
2021-07-23 13:00:37 +08:00
LouisLam
48f82b55f8 prevent unexpected error throw from checkCertificate interrupt the beat 2021-07-23 12:58:05 +08:00
rezzorix
280ba84aca Apple touch icon 192px with preserved transparency
Resized icon.png to 192px but preserved transparency
2021-07-23 12:41:02 +08:00
Louis Lam
0dbecca10f Merge pull request #102 from NiNiyas/pushover-enhancements
Pushover enhancements
2021-07-23 12:32:36 +08:00
Louis Lam
8279368b4d Merge pull request #104 from rezzorix/patch-1
Small grammar updates to Settings.vue
2021-07-23 11:57:32 +08:00
rezzorix
2450b3d082 Small grammar updates to Settings.vue
Just small grammar corrections.
2021-07-23 11:48:39 +08:00
Nelson Chan
6b72d5033a Fix: Fix incorrect error handling 2021-07-23 11:23:43 +08:00
Nelson Chan
4d262bbb6a Fix: Fix no certificate caused by session reuse 2021-07-23 11:22:37 +08:00
Louis Lam
d1370a62bd Merge pull request #103 from Spiritreader/master
Fix parenthesis mistake in notification checker (fixes #86)
2021-07-23 09:11:14 +08:00
Sam
063fd6ef43 Merge branch 'master' of https://github.com/Spiritreader/uptime-kuma 2021-07-22 20:25:08 +02:00
Sam
1d4d7fa9c4 fix parenthesis mistake 2021-07-22 20:25:03 +02:00
Louis Lam
248b5292dc Merge pull request #86 from Spiritreader/master
Implement retries (#56)
2021-07-23 00:11:24 +08:00
Matthew Macdonald-Wallace
47d830db1f Remove examples so they can go on the wiki instead 2021-07-22 16:15:19 +01:00
Matthew Macdonald-Wallace
3a8fbff514 Change casing in README, apply DRY to label values 2021-07-22 16:00:56 +01:00
Matthew Macdonald-Wallace
a93fd274fd Update README to include examples for Prometheus 2021-07-22 15:02:33 +01:00
Niyas
77fbfc23be Pushover enhancements 2021-07-22 19:28:25 +05:30
Matthew Macdonald-Wallace
3b45006567 Move common labels into dedicated const 2021-07-22 14:58:22 +01:00
Niyas
b7a32d4ab6 Pushover enhancements 2021-07-22 19:26:54 +05:30
LouisLam
5a219554b3 grammar 2021-07-22 19:49:46 +08:00
LouisLam
70b1f197c1 rename "Retry Pings" to "Retries" 2021-07-22 19:02:44 +08:00
Matthew Macdonald-Wallace
720051a351 Typo in monitor status name 2021-07-22 11:18:20 +01:00
LouisLam
32a5e838ba add patch3.sql and fix duplicate id in EditMonitor.vue 2021-07-22 17:44:59 +08:00
LouisLam
86e18ac11d Merge branch 'master' into Spiritreader_master
# Conflicts:
#	src/pages/EditMonitor.vue
2021-07-22 17:34:41 +08:00
Matthew Macdonald-Wallace
3dcbae0889 Add labels to metrics for querying 2021-07-22 10:21:20 +01:00
Matthew Macdonald-Wallace
96242dce0d Expose check status and response time to Prometheus 2021-07-22 09:38:27 +01:00
Nelson Chan
f20ab4b0e3 Fix: use Optional chaining 2021-07-22 16:13:58 +08:00
Nelson Chan
96c60dd94a Feat: Add database storage for TLS info 2021-07-22 16:04:32 +08:00
Matthew Macdonald-Wallace
7acb265559 Remove bcryptjs and node-gyp, they should not be here... 2021-07-22 09:01:51 +01:00
Matthew Macdonald-Wallace
582fb2fe29 Export general metrics via the /metrics endpoint 2021-07-22 08:43:04 +01:00
Matthew Macdonald-Wallace
e3d4a896b1 Fix up some formatting 2021-07-22 08:33:21 +01:00
Matthew Macdonald-Wallace
9a1bf6006a Add initial package import and config 2021-07-22 08:24:25 +01:00
Matthew Macdonald-Wallace
ef41a32353 Merge pull request #1 from louislam/master
Pull down upstream
2021-07-22 08:23:24 +01:00
Nelson Chan
ccda6f05f5 Feat: Add Barebones certificate info display 2021-07-22 14:26:43 +08:00
LouisLam
03b3bb5b30 fix if notification throw exception, the heartbeat is not stored in to the db. 2021-07-22 12:28:47 +08:00
LouisLam
7e4a1ad279 remove used vars 2021-07-22 11:15:53 +08:00
LouisLam
916b9da0dc Merge branch 'master' into something
# Conflicts:
#	server/notification.js
#	src/components/NotificationDialog.vue
2021-07-22 11:12:52 +08:00
LouisLam
a64ce81457 update package-lock.json 2021-07-22 10:55:55 +08:00
LouisLam
c575afc8e0 Merge remote-tracking branch 'origin/master' 2021-07-22 10:47:39 +08:00
Louis Lam
1e42343aee Update patch1.sql
minor
2021-07-22 10:45:22 +08:00
LouisLam
afd4cf2425 Merge branch 'master' into simple_pagination 2021-07-22 10:42:30 +08:00
LouisLam
e02eb72863 add db migration 2021-07-22 02:02:35 +08:00
LouisLam
1c0dc18d72 Merge remote-tracking branch 'origin/master' 2021-07-22 01:30:22 +08:00
Louis Lam
c00612c1a9 Update --please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md 2021-07-21 16:29:15 +08:00
Louis Lam
32345fcbe9 Update issue templates 2021-07-21 16:28:31 +08:00
Louis Lam
fd90458e77 Update issue templates 2021-07-21 16:25:58 +08:00
Louis Lam
d89e6f4649 Merge pull request #89 from Saibamen/more_info_in_server_logs
More info in server logs
2021-07-21 11:34:27 +08:00
Adam Stachowicz
c4ca8e2acb More info in server logs 2021-07-21 00:41:38 +02:00
LouisLam
94b5a557bf Merge remote-tracking branch 'origin/master' 2021-07-20 23:43:59 +08:00
Sam
14e1d1f105 add .vscode directory to dockerignore 2021-07-20 17:39:21 +02:00
Sam
8b905b6b12 Indentation fix in editor
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-07-20 17:38:21 +02:00
Louis Lam
fa57d40c3c Update README.md 2021-07-20 20:27:34 +08:00
Louis Lam
62e231e92b Update README.md 2021-07-20 20:27:15 +08:00
LouisLam
02b4dfc100 prevent the telegram getUpdates URL go out of box 2021-07-20 20:18:56 +08:00
Sam
054269ecf0 fix notification when changing from pending -> up 2021-07-20 11:50:33 +02:00
Sam
02230930c5 Merge branch 'master' of https://github.com/Spiritreader/uptime-kuma 2021-07-19 18:26:00 +02:00
Sam
a8b102ad4a add retries for pinging function
backend:
- new field for monitor: maxretries
- new pending status while service is retrying: 2
- pending status event is not marked important
- pending pings however register as downtime in the calculation

frontend:
- added pending status while service is retrying
- added color for new pending status
- added field to configure amount of retries

database:
- IMPORTANT: THIS REQUIRES MIGRATION!!!!
- added field: maxretries with default value 0
2021-07-19 18:23:06 +02:00
Adam Stachowicz
9928ea8c30 Merge branch 'master' into simple_pagination 2021-07-18 16:48:53 +02:00
Adam Stachowicz
2d943620c7 Merge branch 'master' into simple_pagination 2021-07-18 15:37:32 +02:00
Adam Stachowicz
16f363ac38 Merge branch 'master' into simple_pagination 2021-07-18 13:36:57 +02:00
Adam Stachowicz
ce6841eae7 Merge branch 'master' into simple_pagination 2021-07-18 11:46:32 +02:00
Adam Stachowicz
7c94c3b502 Update server/notification.js 2021-07-18 09:42:34 +00:00
Adam Stachowicz
268c8e50f5 Merge branch 'master' into something 2021-07-18 09:42:08 +00:00
Adam Stachowicz
d94894b7e0 Fix require-v-for-key, remove unused declarations and double spaces 2021-07-18 03:10:15 +02:00
Adam Stachowicz
a173700cd4 Add pagination 2021-07-18 03:04:40 +02:00
58 changed files with 6307 additions and 1365 deletions

View File

@@ -1,13 +1,37 @@
/.idea /.idea
/dist /dist
/node_modules /node_modules
/data/kuma.db /data
/.do /.do
**/.dockerignore **/.dockerignore
**/.git **/.git
**/.gitignore **/.gitignore
**/docker-compose* **/docker-compose*
**/Dockerfile* **/[Dd]ockerfile*
LICENSE LICENSE
README.md README.md
.editorconfig .editorconfig
.vscode
.eslint*
.stylelint*
/.github
package-lock.json
yarn.lock
app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
### .gitignore content (commented rules are duplicated)
#node_modules
.DS_Store
#dist
dist-ssr
*.local
#.idea
#/data
#!/data/.gitkeep
#.vscode
### End of .gitignore content

View File

@@ -16,3 +16,6 @@ indent_size = 2
[*.yml] [*.yml]
indent_size = 2 indent_size = 2
[*.vue]
trim_trailing_whitespace = false

73
.eslintrc.js Normal file
View File

@@ -0,0 +1,73 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2020: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@babel/eslint-parser",
sourceType: "module",
requireConfigFile: false,
},
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
"no-unused-vars": "warn",
indent: [
"error",
4,
{
ignoredNodes: ["TemplateLiteral"],
SwitchCase: 1,
},
],
quotes: ["warn", "double"],
//semi: ['off', 'never'],
"vue/html-indent": ["warn", 4], // default: 2
"vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/html-self-closing": "off",
"no-multi-spaces": ["error", {
ignoreEOLComments: true,
}],
"curly": "error",
"object-curly-spacing": ["error", "always"],
"object-curly-newline": "off",
"object-property-newline": "error",
"comma-spacing": "error",
"brace-style": "error",
"no-var": "error",
"key-spacing": "warn",
"keyword-spacing": "warn",
"space-infix-ops": "warn",
"arrow-spacing": "warn",
"no-trailing-spaces": "warn",
"no-constant-condition": ["error", {
"checkLoops": false,
}],
"space-before-blocks": "warn",
//'no-console': 'warn',
"no-extra-boolean-cast": "off",
"no-multiple-empty-lines": ["warn", {
"max": 1,
"maxBOF": 0,
}],
"lines-between-class-members": ["warn", "always", {
exceptAfterSingleLine: true,
}],
"no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"],
//'prefer-template': 'error',
"comma-dangle": ["warn", "only-multiline"],
},
}

10
.github/ISSUE_TEMPLATE/ask-for-help.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Ask for help
about: You can ask any question related to Uptime Kuma.
title: ''
labels: help
assignees: ''
---

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ dist-ssr
/data /data
!/data/.gitkeep !/data/.gitkeep
.vscode

3
.stylelintrc Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "stylelint-config-recommended",
}

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
louis@uptimekuma.louislam.net.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

104
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,104 @@
# Project Info
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
# Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
- All settings in frontend.
- Easy to use
# Tools
- Node.js >= 14
- Git
- IDE that supports .editorconfig (I am using Intellji Idea)
- A SQLite tool (I am using SQLite Expert Personal)
# Prepare the dev
```bash
npm install
```
# Backend Dev
```bash
npm run start-server
# Or
node server/server.js
```
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.)
# Frontend Dev
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
```bash
npm run dev
```
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
You can use Vue Devtool Chrome extension for debugging.
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
```javascript
localStorage.dev = "dev";
```
So that the frontend will try to connect websocket server in 3001.
Alternately, you can specific NODE_ENV to "development".
## Build the frontend
```bash
npm run build
```
## Frontend Details
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
The router in "src/main.js"
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
The data and socket logic in "src/mixins/socket.js"
# Database Migration
TODO
# Unit Test
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.

View File

@@ -15,12 +15,12 @@ It is a self-hosted monitoring tool like "Uptime Robot".
* Monitoring uptime for HTTP(s) / TCP / Ping. * Monitoring uptime for HTTP(s) / TCP / Ping.
* Fancy, Reactive, Fast UI/UX. * Fancy, Reactive, Fast UI/UX.
* Notifications via Webhook, Telegram, Discord and email (SMTP). * Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
* 20 seconds interval. * 20 seconds interval.
# How to Use # How to Use
### Docker ## Docker
```bash ```bash
# Create a volume # Create a volume
@@ -38,9 +38,9 @@ Change Port and Volume
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1 docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
``` ```
### Without Docker ## Without Docker
Required Tools: Node.js >= 14, git and pm2. Required Tools: Node.js >= 14, git and pm2.
```bash ```bash
git clone https://github.com/louislam/uptime-kuma.git git clone https://github.com/louislam/uptime-kuma.git
@@ -62,12 +62,25 @@ pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after started.
### One-click Deploy to DigitalOcean
## (Optional) One more step for Reverse Proxy
This is optional for someone who want to do reverse proxy.
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket.
Please read wiki for more info:
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
## One-click Deploy
<!---
Abort. Heroku instance killed the server.js if idle, stupid.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.9)
-->
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
Choose Cheapest Plan is enough. (US$ 5)
# How to Update # How to Update
### Docker ### Docker
@@ -80,12 +93,17 @@ PS: For every new release, it takes some time to build the docker image, please
```bash ```bash
git fetch --all git fetch --all
git checkout 1.0.6 --force git checkout 1.0.9 --force
npm install npm install
npm run build npm run build
pm2 restart uptime-kuma pm2 restart uptime-kuma
``` ```
# What's Next?
I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
# More Screenshots # More Screenshots
Settings Page: Settings Page:
@@ -99,10 +117,10 @@ Telegram Notification Sample:
# Motivation # Motivation
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained. * I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
* Want to build a fancy UI. * Want to build a fancy UI.
* Learn Vue 3 and vite.js. * Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5. * Show the power of Bootstrap 5.
* Try to use WebSocket with SPA instead of REST API. * Try to use WebSocket with SPA instead of REST API.
* Deploy my first Docker image to Docker Hub. * Deploy my first Docker image to Docker Hub.
@@ -114,6 +132,6 @@ If you love this project, please consider giving me a ⭐.
If you want to report a bug or request a new feature. Free feel to open a new issue. If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/wiki/%5BDev%5D-Setup-Development-Environment If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki. English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki.

7
app.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "Uptime Kuma",
"description": "A fancy self-hosted monitoring tool",
"repository": "https://github.com/louislam/uptime-kuma",
"logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
"keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"]
}

Binary file not shown.

37
db/patch1.sql Normal file
View File

@@ -0,0 +1,37 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
-- SQL Generated by Intellij Idea
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
create table monitor_dg_tmp
(
id INTEGER not null
primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER
references user
on update cascade on delete set null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME,
keyword VARCHAR(255)
);
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
drop table monitor;
alter table monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys=on;

9
db/patch2.sql Normal file
View File

@@ -0,0 +1,9 @@
BEGIN TRANSACTION;
CREATE TABLE monitor_tls_info (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
info_json TEXT
);
COMMIT;

37
db/patch3.sql Normal file
View File

@@ -0,0 +1,37 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
-- Add maxretries column to monitor
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
create table monitor_dg_tmp
(
id INTEGER not null
primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER
references user
on update cascade on delete set null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0
);
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
drop table monitor;
alter table monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys=on;

40
db/patch4.sql Normal file
View File

@@ -0,0 +1,40 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
-- OK.... serious wrong, missing maxretries column
-- Developers should patch it manually if you have missing the maxretries column
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
create table monitor_dg_tmp
(
id INTEGER not null
primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER
references user
on update cascade on delete set null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0,
ignore_tls BOOLEAN default 0 not null,
upside_down BOOLEAN default 0 not null
);
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor;
drop table monitor;
alter table monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys=on;

70
db/patch5.sql Normal file
View File

@@ -0,0 +1,70 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
create table monitor_dg_tmp (
id INTEGER not null primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER references user on update cascade on delete
set
null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME default (DATETIME('now')) not null,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0,
ignore_tls BOOLEAN default 0 not null,
upside_down BOOLEAN default 0 not null
);
insert into
monitor_dg_tmp(
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
keyword,
maxretries,
ignore_tls,
upside_down
)
select
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
keyword,
maxretries,
ignore_tls,
upside_down
from
monitor;
drop table monitor;
alter table
monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys = on;

View File

@@ -11,26 +11,16 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
# Touching above code may causes sqlite3 re-compile again, painful slow. # Touching above code may causes sqlite3 re-compile again, painful slow.
# Install apprise # Install apprise
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/ RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev RUN pip3 --no-cache-dir install apprise && \
# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo rm -rf /root/.cache
# Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev
# Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev
# Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev
# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six
# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN apk add --no-cache python3 py3-pip py3-six cargo
RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev python3-dev && \
pip3 install apprise && \
apk del .build-deps
RUN apprise --version
# New things add here # New things add here
COPY . . COPY . .
RUN npm install RUN npm install && \
RUN npm run build npm run build && \
npm prune
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]

3387
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,33 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.0.6", "version": "1.0.9",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/louislam/uptime-kuma.git" "url": "https://github.com/louislam/uptime-kuma.git"
}, },
"engines": {
"node": "14.*"
},
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"update": "", "update": "",
"build": "vite build", "build": "vite build",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.6 --target release . --push", "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.9 --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.6 && npm install && npm run build", "setup": "git checkout 1.0.9 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js", "version-global-replace": "node extra/version-global-replace.js",
"mark-as-nightly": "node extra/mark-as-nightly.js" "mark-as-nightly": "node extra/mark-as-nightly.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"args-parser": "^1.3.0", "args-parser": "^1.3.0",
"axios": "^0.21.1", "axios": "^0.21.1",
@@ -28,27 +36,39 @@
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2", "http-graceful-shutdown": "^3.1.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"password-hash": "^1.2.2", "password-hash": "^1.2.2",
"prom-client": "^13.1.0",
"prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.20", "redbean-node": "0.0.20",
"socket.io": "^4.1.3", "socket.io": "^4.1.3",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6",
"vue": "^3.0.5", "vue": "^3.0.5",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10", "vue-router": "^4.0.10",
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-legacy": "^1.4.4", "@babel/eslint-parser": "^7.13.10",
"@vitejs/plugin-vue": "^1.2.5", "@types/bootstrap": "^5.0.17",
"@vitejs/plugin-legacy": "^1.5.0",
"@vitejs/plugin-vue": "^1.3.0",
"@vue/compiler-sfc": "^3.1.5", "@vue/compiler-sfc": "^3.1.5",
"core-js": "^3.15.2", "core-js": "^3.15.2",
"sass": "^1.35.2", "eslint": "^7.31.0",
"vite": "^2.4.2" "eslint-plugin-vue": "^7.14.0",
"sass": "^1.36.0",
"stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.3.5",
"vite": "^2.4.4"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

51
server/auth.js Normal file
View File

@@ -0,0 +1,51 @@
const basicAuth = require("express-basic-auth")
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { debug } = require("../src/util");
/**
*
* @param username : string
* @param password : string
* @returns {Promise<Bean|null>}
*/
exports.login = async function (username, password) {
let user = await R.findOne("user", " username = ? AND active = 1 ", [
username,
])
if (user && passwordHash.verify(password, user.password)) {
// Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password),
user.id,
]);
}
return user;
}
return null;
}
function myAuthorizer(username, password, callback) {
setting("disableAuth").then((result) => {
if (result) {
callback(null, true)
} else {
exports.login(username, password).then((user) => {
callback(null, user != null)
})
}
})
}
exports.basicAuth = basicAuth({
authorizer: myAuthorizer,
authorizeAsync: true,
challenge: true,
});

120
server/database.js Normal file
View File

@@ -0,0 +1,120 @@
const fs = require("fs");
const { sleep } = require("../src/util");
const { R } = require("redbean-node");
const {
setSetting, setting,
} = require("./util-server");
class Database {
static templatePath = "./db/kuma.db"
static path = "./data/kuma.db";
static latestVersion = 5;
static noReject = true;
static async patch() {
let version = parseInt(await setting("database_version"));
if (! version) {
version = 0;
}
console.info("Your database version: " + version);
console.info("Latest database version: " + this.latestVersion);
if (version === this.latestVersion) {
console.info("Database no need to patch");
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = "./data/kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
// Try catch anything here, if gone wrong, restore the backup
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`;
console.info(`Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile);
console.info(`Patched ${sqlFile}`);
await setSetting("database_version", i);
}
console.log("Database Patched Successfully");
} catch (ex) {
await Database.close();
console.error("Patch db failed!!! Restoring the backup")
fs.copyFileSync(backupPath, Database.path);
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed")
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
process.exit(1);
}
}
}
/**
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
* @param filename
* @returns {Promise<void>}
*/
static async importSQLFile(filename) {
await R.getCell("SELECT 1");
let text = fs.readFileSync(filename).toString();
// Remove all comments (--)
let lines = text.split("\n");
lines = lines.filter((line) => {
return ! line.startsWith("--")
});
// Split statements by semicolon
// Filter out empty line
text = lines.join("\n")
let statements = text.split(";")
.map((statement) => {
return statement.trim();
})
.filter((statement) => {
return statement !== "";
})
for (let statement of statements) {
await R.exec(statement);
}
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught
* @returns {Promise<void>}
*/
static async close() {
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB")
while (true) {
Database.noReject = true;
await R.close()
await sleep(2000)
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db")
}
}
console.log("SQLite closed")
process.removeListener("unhandledRejection", listener);
}
}
module.exports = Database;

View File

@@ -1,17 +1,15 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require("dayjs/plugin/utc")
var timezone = require('dayjs/plugin/timezone') let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const axios = require("axios"); const { BeanModel } = require("redbean-node/dist/bean-model");
const {R} = require("redbean-node");
const {BeanModel} = require("redbean-node/dist/bean-model");
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING
*/ */
class Heartbeat extends BeanModel { class Heartbeat extends BeanModel {

View File

@@ -1,28 +1,30 @@
const https = require("https");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require("dayjs/plugin/utc")
var timezone = require('dayjs/plugin/timezone') let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const {tcping, ping} = require("../util-server"); const { Prometheus } = require("../prometheus");
const {R} = require("redbean-node"); const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
const {BeanModel} = require("redbean-node/dist/bean-model"); const { tcping, ping, checkCertificate } = require("../util-server");
const {Notification} = require("../notification") const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification")
/** /**
* status: * status:
* 0 = DOWN * 0 = DOWN
* 1 = UP * 1 = UP
* 2 = PENDING
*/ */
class Monitor extends BeanModel { class Monitor extends BeanModel {
async toJSON() { async toJSON() {
let notificationIDList = {}; let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [ let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id this.id,
]) ])
for (let bean of list) { for (let bean of list) {
@@ -35,35 +37,62 @@ class Monitor extends BeanModel {
url: this.url, url: this.url,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries,
weight: this.weight, weight: this.weight,
active: this.active, active: this.active,
type: this.type, type: this.type,
interval: this.interval, interval: this.interval,
keyword: this.keyword, keyword: this.keyword,
notificationIDList ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
notificationIDList,
}; };
} }
/**
* Parse to boolean
* @returns {boolean}
*/
getIgnoreTls() {
return Boolean(this.ignoreTls)
}
/**
* Parse to boolean
* @returns {boolean}
*/
isUpsideDown() {
return Boolean(this.upsideDown);
}
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0;
let prometheus = new Prometheus(this);
const beat = async () => { const beat = async () => {
console.log(`Monitor ${this.id}: Heartbeat`)
if (! previousBeat) { if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id this.id,
]) ])
} }
const isFirstBeat = !previousBeat;
let bean = R.dispense("heartbeat") let bean = R.dispense("heartbeat")
bean.monitor_id = this.id; bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTime(dayjs.utc());
bean.status = 0; bean.status = DOWN;
if (this.isUpsideDown()) {
bean.status = flipStatus(bean.status);
}
// Duration // Duration
if (previousBeat) { if (! isFirstBeat) {
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second'); bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
} else { } else {
bean.duration = 0; bean.duration = 0;
} }
@@ -71,14 +100,36 @@ class Monitor extends BeanModel {
try { try {
if (this.type === "http" || this.type === "keyword") { if (this.type === "http" || this.type === "keyword") {
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
// Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, { let res = await axios.get(this.url, {
headers: { 'User-Agent':'Uptime-Kuma' } headers: {
}) "User-Agent": "Uptime-Kuma",
},
httpsAgent: new https.Agent({
maxCachedSessions: 0,
rejectUnauthorized: ! this.getIgnoreTls(),
}),
});
bean.msg = `${res.status} - ${res.statusText}` bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") {
try {
await this.updateTlsInfo(checkCertificate(res));
} catch (e) {
console.error(e.message)
}
}
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
if (this.type === "http") { if (this.type === "http") {
bean.status = 1; bean.status = UP;
} else { } else {
let data = res.data; let data = res.data;
@@ -90,43 +141,77 @@ class Monitor extends BeanModel {
if (data.includes(this.keyword)) { if (data.includes(this.keyword)) {
bean.msg += ", keyword is found" bean.msg += ", keyword is found"
bean.status = 1; bean.status = UP;
} else { } else {
throw new Error(bean.msg + ", but keyword is not found") throw new Error(bean.msg + ", but keyword is not found")
} }
} }
} else if (this.type === "port") { } else if (this.type === "port") {
bean.ping = await tcping(this.hostname, this.port); bean.ping = await tcping(this.hostname, this.port);
bean.msg = "" bean.msg = ""
bean.status = 1; bean.status = UP;
} else if (this.type === "ping") { } else if (this.type === "ping") {
bean.ping = await ping(this.hostname); bean.ping = await ping(this.hostname);
bean.msg = "" bean.msg = ""
bean.status = 1; bean.status = UP;
} }
if (this.isUpsideDown()) {
bean.status = flipStatus(bean.status);
if (bean.status === DOWN) {
throw new Error("Flip UP to DOWN");
}
}
retries = 0;
} catch (error) { } catch (error) {
bean.msg = error.message; bean.msg = error.message;
// If UP come in here, it must be upside down mode
// Just reset the retries
if (this.isUpsideDown() && bean.status === UP) {
retries = 0;
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
retries++;
bean.status = PENDING;
}
} }
// Mark as important if status changed // * ? -> ANY STATUS = important [isFirstBeat]
if (! previousBeat || previousBeat.status !== bean.status) { // UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeat.status === UP && bean.status === DOWN) ||
(previousBeat.status === DOWN && bean.status === UP) ||
(previousBeat.status === PENDING && bean.status === DOWN);
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
if (isImportant) {
bean.important = true; bean.important = true;
// Do not send if first beat is UP // Send only if the first beat is DOWN
if (previousBeat || bean.status !== 1) { if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [ let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
this.id this.id,
]) ])
let promiseList = [];
let text; let text;
if (bean.status === 1) { if (bean.status === UP) {
text = "✅ Up" text = "✅ Up"
} else { } else {
text = "🔴 Down" text = "🔴 Down"
@@ -134,17 +219,29 @@ class Monitor extends BeanModel {
let msg = `[${this.name}] [${text}] ${bean.msg}`; let msg = `[${this.name}] [${text}] ${bean.msg}`;
for(let notification of notificationList) { for (let notification of notificationList) {
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())); try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
}
} }
await Promise.all(promiseList);
} }
} else { } else {
bean.important = false; bean.important = false;
} }
if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
} else if (bean.status === PENDING) {
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
} else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
}
prometheus.update(bean)
io.to(this.user_id).emit("heartbeat", bean.toJSON()); io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean) await R.store(bean)
@@ -161,10 +258,42 @@ class Monitor extends BeanModel {
clearInterval(this.heartbeatInterval) clearInterval(this.heartbeatInterval)
} }
/**
* Helper Method:
* returns URL object for further usage
* returns null if url is invalid
* @returns {null|URL}
*/
getUrl() {
try {
return new URL(this.url);
} catch (_) {
return null;
}
}
/**
* Store TLS info to database
* @param checkCertificateResult
* @returns {Promise<void>}
*/
async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
this.id,
]);
if (tls_info_bean == null) {
tls_info_bean = R.dispense("monitor_tls_info");
tls_info_bean.monitor_id = this.id;
}
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean);
}
static async sendStats(io, monitorID, userID) { static async sendStats(io, monitorID, userID) {
Monitor.sendAvgPing(24, io, monitorID, userID); Monitor.sendAvgPing(24, io, monitorID, userID);
Monitor.sendUptime(24, io, monitorID, userID); Monitor.sendUptime(24, io, monitorID, userID);
Monitor.sendUptime(24 * 30, io, monitorID, userID); Monitor.sendUptime(24 * 30, io, monitorID, userID);
Monitor.sendCertInfo(io, monitorID, userID);
} }
/** /**
@@ -179,12 +308,21 @@ class Monitor extends BeanModel {
AND ping IS NOT NULL AND ping IS NOT NULL
AND monitor_id = ? `, [ AND monitor_id = ? `, [
-duration, -duration,
monitorID monitorID,
])); ]));
io.to(userID).emit("avgPing", monitorID, avgPing); io.to(userID).emit("avgPing", monitorID, avgPing);
} }
static async sendCertInfo(io, monitorID, userID) {
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
monitorID,
]);
if (tls_info != null) {
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
}
}
/** /**
* Uptime with calculation * Uptime with calculation
* Calculation based on: * Calculation based on:
@@ -200,7 +338,7 @@ class Monitor extends BeanModel {
WHERE time > DATETIME('now', ? || ' hours') WHERE time > DATETIME('now', ? || ' hours')
AND monitor_id = ? `, [ AND monitor_id = ? `, [
-duration, -duration,
monitorID monitorID,
]); ]);
let downtime = 0; let downtime = 0;
@@ -224,7 +362,7 @@ class Monitor extends BeanModel {
// Handle if heartbeat duration longer than the target duration // Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) { if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), 'second'); let trim = dayjs.utc().diff(dayjs(time), "second");
value = sec - trim; value = sec - trim;
if (value < 0) { if (value < 0) {
@@ -233,7 +371,7 @@ class Monitor extends BeanModel {
} }
total += value; total += value;
if (row.status === 0) { if (row.status === 0 || row.status === 2) {
downtime += value; downtime += value;
} }
} }
@@ -245,8 +383,6 @@ class Monitor extends BeanModel {
} }
} }
io.to(userID).emit("uptime", monitorID, duration, uptime); io.to(userID).emit("uptime", monitorID, duration, uptime);
} }
} }

View File

@@ -1,6 +1,6 @@
const axios = require("axios"); const axios = require("axios");
const {R} = require("redbean-node"); const { R } = require("redbean-node");
const FormData = require('form-data'); const FormData = require("form-data");
const nodemailer = require("nodemailer"); const nodemailer = require("nodemailer");
const child_process = require("child_process"); const child_process = require("child_process");
@@ -24,7 +24,7 @@ class Notification {
params: { params: {
chat_id: notification.telegramChatID, chat_id: notification.telegramChatID,
text: msg, text: msg,
} },
}) })
return okMsg; return okMsg;
@@ -41,7 +41,7 @@ class Notification {
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg, "message": msg,
"priority": notification.gotifyPriority || 8, "priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma" "title": "Uptime-Kuma",
}) })
return okMsg; return okMsg;
@@ -62,17 +62,17 @@ class Notification {
if (notification.webhookContentType === "form-data") { if (notification.webhookContentType === "form-data") {
finalData = new FormData(); finalData = new FormData();
finalData.append('data', JSON.stringify(data)); finalData.append("data", JSON.stringify(data));
config = { config = {
headers: finalData.getHeaders() headers: finalData.getHeaders(),
} }
} else { } else {
finalData = data; finalData = data;
} }
let res = await axios.post(notification.webhookURL, finalData, config) await axios.post(notification.webhookURL, finalData, config)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
@@ -84,137 +84,166 @@ class Notification {
} else if (notification.type === "discord") { } else if (notification.type === "discord") {
try { try {
// If heartbeatJSON is null, assume we're testing. // If heartbeatJSON is null, assume we're testing.
if(heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let data = {
username: 'Uptime-Kuma', username: "Uptime-Kuma",
content: msg content: msg,
}
let res = await axios.post(notification.discordWebhookUrl, data)
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if(heartbeatJSON['status'] == 0) {
var alertColor = "16711680";
} else if(heartbeatJSON['status'] == 1) {
var alertColor = "65280";
}
let data = {
username: 'Uptime-Kuma',
embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"]
},
{
name: "Message",
value: msg
} }
] await axios.post(notification.discordWebhookUrl, data)
}] return okMsg;
} }
let res = await axios.post(notification.discordWebhookUrl, data) // If heartbeatJSON is not null, we go into the normal alerting loop.
return okMsg; if (heartbeatJSON["status"] == 0) {
} catch(error) { var alertColor = "16711680";
throwGeneralAxiosError(error) } else if (heartbeatJSON["status"] == 1) {
var alertColor = "65280";
}
let data = {
username: "Uptime-Kuma",
embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Message",
value: msg,
},
],
}],
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
} }
} else if (notification.type === "signal") { } else if (notification.type === "signal") {
try { try {
let data = { let data = {
"message": msg, "message": msg,
"number": notification.signalNumber, "number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, '').split(",") "recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
}; };
let config = {}; let config = {};
let res = await axios.post(notification.signalURL, data, config) await axios.post(notification.signalURL, data, config)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "pushy") {
try {
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
"message": "Uptime-Kuma"
},
"notification": {
"body": msg,
"badge": 1,
"sound": "ping.aiff"
}
})
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "slack") { } else if (notification.type === "slack") {
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo} let data = {
let res = await axios.post(notification.slackwebhookURL, data) "text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data)
return okMsg; return okMsg;
} }
const time = heartbeatJSON["time"]; const time = heartbeatJSON["time"];
let data = { let data = {
"text": "Uptime Kuma Alert", "text": "Uptime Kuma Alert",
"channel":notification.slackchannel, "channel": notification.slackchannel,
"username": notification.slackusername, "username": notification.slackusername,
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,
"blocks": [{ "blocks": [{
"type": "header", "type": "header",
"text": { "text": {
"type": "plain_text", "type": "plain_text",
"text": "Uptime Kuma Alert" "text": "Uptime Kuma Alert",
} },
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
}, },
{ {
"type": "section", "type": "mrkdwn",
"fields": [{ "text": "*Time (UTC)*\n" + time,
"type": "mrkdwn", }],
"text": '*Message*\n'+msg },
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
}, },
{ "value": "Uptime-Kuma",
"type": "mrkdwn", "url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
"text": "*Time (UTC)*\n"+time },
} ],
] }],
}, }
{ await axios.post(notification.slackwebhookURL, data)
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma"
}
]
}
]
}
let res = await axios.post(notification.slackwebhookURL, data)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "pushover") { } else if (notification.type === "pushover") {
var pushoverlink = 'https://api.pushover.net/1/messages.json' let pushoverlink = "https://api.pushover.net/1/messages.json"
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>", let data = {
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds, "message": "<b>Uptime Kuma Pushover testing successful.</b>",
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1} "user": notification.pushoveruserkey,
let res = await axios.post(pushoverlink, data) "token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg; return okMsg;
} }
let data = { let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" +msg + '\n<b>Time (UTC)</b>:' +time, "message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user":notification.pushoveruserkey, "user": notification.pushoveruserkey,
"token": notification.pushoverapptoken, "token": notification.pushoverapptoken,
"sound": notification.pushoversounds, "sound": notification.pushoversounds,
"priority": notification.pushoverpriority, "priority": notification.pushoverpriority,
"title": notification.pushovertitle, "title": notification.pushovertitle,
"retry": "30", "retry": "30",
"expire": "3600", "expire": "3600",
"html": 1 "html": 1,
} }
let res = await axios.post(pushoverlink, data) await axios.post(pushoverlink, data)
return okMsg; return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
@@ -224,6 +253,41 @@ class Notification {
return Notification.apprise(notification, msg) return Notification.apprise(notification, msg)
} else if (notification.type === "lunasea") {
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
return okMsg;
}
if (heartbeatJSON["status"] == 0) {
let downdata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
return okMsg;
}
if (heartbeatJSON["status"] == 1) {
let updata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
return okMsg;
}
} catch (error) {
throwGeneralAxiosError(error)
}
} else { } else {
throw new Error("Notification type is not supported") throw new Error("Notification type is not supported")
} }
@@ -278,7 +342,7 @@ class Notification {
}); });
// send mail with defined transport object // send mail with defined transport object
let info = await transporter.sendMail({ await transporter.sendMail({
from: `"Uptime Kuma" <${notification.smtpFrom}>`, from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo, to: notification.smtpTo,
subject: msg, subject: msg,
@@ -291,24 +355,23 @@ class Notification {
static async apprise(notification, msg) { static async apprise(notification, msg) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found';
if (output) { if (output) {
if (! output.includes("ERROR")) { if (! output.includes("ERROR")) {
return "Sent Successfully"; return "Sent Successfully";
} else {
throw new Error(output)
} }
throw new Error(output)
} else { } else {
return "" return ""
} }
} }
static checkApprise() { static checkApprise() {
let commandExistsSync = require('command-exists').sync; let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync('apprise'); let exists = commandExistsSync("apprise");
return exists; return exists;
} }

View File

@@ -1,5 +1,5 @@
const passwordHashOld = require('password-hash'); const passwordHashOld = require("password-hash");
const bcrypt = require('bcrypt'); const bcrypt = require("bcrypt");
const saltRounds = 10; const saltRounds = 10;
exports.generate = function (password) { exports.generate = function (password) {
@@ -9,9 +9,9 @@ exports.generate = function (password) {
exports.verify = function (password, hash) { exports.verify = function (password, hash) {
if (isSHA1(hash)) { if (isSHA1(hash)) {
return passwordHashOld.verify(password, hash) return passwordHashOld.verify(password, hash)
} else {
return bcrypt.compareSync(password, hash);
} }
return bcrypt.compareSync(password, hash);
} }
function isSHA1(hash) { function isSHA1(hash) {

View File

@@ -1,9 +1,9 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js // https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows // Fixed on Windows
var spawn = require('child_process').spawn, let spawn = require("child_process").spawn,
events = require('events'), events = require("events"),
fs = require('fs'), fs = require("fs"),
WIN = /^win/.test(process.platform), WIN = /^win/.test(process.platform),
LIN = /^linux/.test(process.platform), LIN = /^linux/.test(process.platform),
MAC = /^darwin/.test(process.platform); MAC = /^darwin/.test(process.platform);
@@ -11,8 +11,9 @@ var spawn = require('child_process').spawn,
module.exports = Ping; module.exports = Ping;
function Ping(host, options) { function Ping(host, options) {
if (!host) if (!host) {
throw new Error('You must specify a host to ping!'); throw new Error("You must specify a host to ping!");
}
this._host = host; this._host = host;
this._options = options = (options || {}); this._options = options = (options || {});
@@ -20,26 +21,24 @@ function Ping(host, options) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
if (WIN) { if (WIN) {
this._bin = 'c:/windows/system32/ping.exe'; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} } else if (LIN) {
else if (LIN) { this._bin = "/bin/ping";
this._bin = '/bin/ping'; this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
this._args = (options.args) ? options.args : [ '-n', '-w', '2', '-c', '1', host ];
this._regmatch = /=([0-9.]+?) ms/; // need to verify this this._regmatch = /=([0-9.]+?) ms/; // need to verify this
} } else if (MAC) {
else if (MAC) { this._bin = "/sbin/ping";
this._bin = '/sbin/ping'; this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
this._args = (options.args) ? options.args : [ '-n', '-t', '2', '-c', '1', host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} } else {
else { throw new Error("Could not detect your ping binary.");
throw new Error('Could not detect your ping binary.');
} }
if (!fs.existsSync(this._bin)) if (!fs.existsSync(this._bin)) {
throw new Error('Could not detect '+this._bin+' on your system'); throw new Error("Could not detect " + this._bin + " on your system");
}
this._i = 0; this._i = 0;
@@ -51,48 +50,57 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING // SEND A PING
// =========== // ===========
Ping.prototype.send = function(callback) { Ping.prototype.send = function(callback) {
var self = this; let self = this;
callback = callback || function(err, ms) { callback = callback || function(err, ms) {
if (err) return self.emit('error', err); if (err) {
else return self.emit('result', ms); return self.emit("error", err);
}
return self.emit("result", ms);
}; };
var _ended, _exited, _errored; let _ended, _exited, _errored;
this._ping = spawn(this._bin, this._args); // spawn the binary this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping.on('error', function(err) { // handle binary errors this._ping.on("error", function(err) { // handle binary errors
_errored = true; _errored = true;
callback(err); callback(err);
}); });
this._ping.stdout.on('data', function(data) { // log stdout this._ping.stdout.on("data", function(data) { // log stdout
this._stdout = (this._stdout || '') + data; this._stdout = (this._stdout || "") + data;
}); });
this._ping.stdout.on('end', function() { this._ping.stdout.on("end", function() {
_ended = true; _ended = true;
if (_exited && !_errored) onEnd.call(self._ping); if (_exited && !_errored) {
onEnd.call(self._ping);
}
}); });
this._ping.stderr.on('data', function(data) { // log stderr this._ping.stderr.on("data", function(data) { // log stderr
this._stderr = (this._stderr || '') + data; this._stderr = (this._stderr || "") + data;
}); });
this._ping.on('exit', function(code) { // handle complete this._ping.on("exit", function(code) { // handle complete
_exited = true; _exited = true;
if (_ended && !_errored) onEnd.call(self._ping); if (_ended && !_errored) {
onEnd.call(self._ping);
}
}); });
function onEnd() { function onEnd() {
var stdout = this.stdout._stdout, let stdout = this.stdout._stdout,
stderr = this.stderr._stderr, stderr = this.stderr._stderr,
ms; ms;
if (stderr) if (stderr) {
return callback(new Error(stderr)); return callback(new Error(stderr));
else if (!stdout) }
return callback(new Error('No stdout detected'));
if (!stdout) {
return callback(new Error("No stdout detected"));
}
ms = stdout.match(self._regmatch); // parse out the ##ms response ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms; ms = (ms && ms[1]) ? Number(ms[1]) : ms;
@@ -104,7 +112,7 @@ Ping.prototype.send = function(callback) {
// CALL Ping#send(callback) ON A TIMER // CALL Ping#send(callback) ON A TIMER
// =================================== // ===================================
Ping.prototype.start = function(callback) { Ping.prototype.start = function(callback) {
var self = this; let self = this;
this._i = setInterval(function() { this._i = setInterval(function() {
self.send(callback); self.send(callback);
}, (self._options.interval || 5000)); }, (self._options.interval || 5000));

59
server/prometheus.js Normal file
View File

@@ -0,0 +1,59 @@
const PrometheusClient = require('prom-client');
const commonLabels = [
'monitor_name',
'monitor_type',
'monitor_url',
'monitor_hostname',
'monitor_port',
]
const monitor_response_time = new PrometheusClient.Gauge({
name: 'monitor_response_time',
help: 'Monitor Response Time (ms)',
labelNames: commonLabels
});
const monitor_status = new PrometheusClient.Gauge({
name: 'monitor_status',
help: 'Monitor Status (1 = UP, 0= DOWN)',
labelNames: commonLabels
});
class Prometheus {
monitorLabelValues = {}
constructor(monitor) {
this.monitorLabelValues = {
monitor_name: monitor.name,
monitor_type: monitor.type,
monitor_url: monitor.url,
monitor_hostname: monitor.hostname,
monitor_port: monitor.port
}
}
update(heartbeat) {
try {
monitor_status.set(this.monitorLabelValues, heartbeat.status)
} catch (e) {
console.error(e)
}
try {
if (typeof heartbeat.ping === 'number') {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
} else {
// Is it good?
monitor_response_time.set(this.monitorLabelValues, -1)
}
} catch (e) {
console.error(e)
}
}
}
module.exports = {
Prometheus
}

View File

@@ -1,23 +1,46 @@
console.log("Welcome to Uptime Kuma ") console.log("Welcome to Uptime Kuma")
console.log("Importing libraries")
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const dayjs = require("dayjs");
const {R} = require("redbean-node");
const passwordHash = require('./password-hash');
const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor");
const fs = require("fs");
const {getSettings} = require("./util-server");
const {Notification} = require("./notification")
const gracefulShutdown = require('http-graceful-shutdown');
const {sleep} = require("./util");
const args = require('args-parser')(process.argv);
const version = require('../package.json').version; const { sleep, debug } = require("../src/util");
const hostname = args.host || "0.0.0.0"
const port = args.port || 3001 console.log("Importing Node libraries")
const fs = require("fs");
const http = require("http");
console.log("Importing 3rd-party libraries")
debug("Importing express");
const express = require("express");
debug("Importing socket.io");
const { Server } = require("socket.io");
debug("Importing dayjs");
const dayjs = require("dayjs");
debug("Importing redbean-node");
const { R } = require("redbean-node");
debug("Importing jsonwebtoken");
const jwt = require("jsonwebtoken");
debug("Importing http-graceful-shutdown");
const gracefulShutdown = require("http-graceful-shutdown");
debug("Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics");
console.log("Importing this project modules");
debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting } = require("./util-server");
debug("Importing Notification");
const { Notification } = require("./notification");
debug("Importing Database");
const Database = require("./database");
const { basicAuth } = require("./auth");
const { login } = require("./auth");
const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv);
const version = require("../package.json").version;
const hostname = process.env.HOST || args.host || "0.0.0.0"
const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version) console.info("Version: " + version)
@@ -27,24 +50,58 @@ const server = http.createServer(app);
const io = new Server(server); const io = new Server(server);
app.use(express.json()) app.use(express.json())
/**
* Total WebSocket client connected to server currently, no actual use
* @type {number}
*/
let totalClient = 0; let totalClient = 0;
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null; let jwtSecret = null;
/**
* Main monitor list
* @type {{}}
*/
let monitorList = {}; let monitorList = {};
/**
* Show Setup Page
* @type {boolean}
*/
let needSetup = false; let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = fs.readFileSync("./dist/index.html").toString();
(async () => { (async () => {
await initDatabase(); await initDatabase();
console.log("Adding route") console.log("Adding route")
app.use('/', express.static("dist"));
app.get('*', function(request, response, next) { // Normal Router here
response.sendFile(process.cwd() + '/dist/index.html');
app.use("/", express.static("dist"));
// Basic Auth Router here
// Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics())
// Universal Route Handler, must be at the end
app.get("*", function(request, response, next) {
response.end(indexHTML)
}); });
console.log("Adding socket handler") console.log("Adding socket handler")
io.on('connection', async (socket) => { io.on("connection", async (socket) => {
socket.emit("info", { socket.emit("info", {
version, version,
@@ -57,11 +114,13 @@ let needSetup = false;
socket.emit("setup") socket.emit("setup")
} }
socket.on('disconnect', () => { socket.on("disconnect", () => {
totalClient--; totalClient--;
}); });
// ***************************
// Public API // Public API
// ***************************
socket.on("loginByToken", async (token, callback) => { socket.on("loginByToken", async (token, callback) => {
@@ -71,25 +130,29 @@ let needSetup = false;
console.log("Username from JWT: " + decoded.username) console.log("Username from JWT: " + decoded.username)
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await R.findOne("user", " username = ? AND active = 1 ", [
decoded.username decoded.username,
]) ])
if (user) { if (user) {
debug("afterLogin")
await afterLogin(socket, user) await afterLogin(socket, user)
debug("afterLogin ok")
callback({ callback({
ok: true, ok: true,
}) })
} else { } else {
callback({ callback({
ok: false, ok: false,
msg: "The user is inactive or deleted." msg: "The user is inactive or deleted.",
}) })
} }
} catch (error) { } catch (error) {
callback({ callback({
ok: false, ok: false,
msg: "Invalid token." msg: "Invalid token.",
}) })
} }
@@ -98,32 +161,21 @@ let needSetup = false;
socket.on("login", async (data, callback) => { socket.on("login", async (data, callback) => {
console.log("Login") console.log("Login")
let user = await R.findOne("user", " username = ? AND active = 1 ", [ let user = await login(data.username, data.password)
data.username
])
if (user && passwordHash.verify(data.password, user.password)) {
// Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(data.password),
user.id
]);
}
if (user) {
await afterLogin(socket, user) await afterLogin(socket, user)
callback({ callback({
ok: true, ok: true,
token: jwt.sign({ token: jwt.sign({
username: data.username username: data.username,
}, jwtSecret) }, jwtSecret),
}) })
} else { } else {
callback({ callback({
ok: false, ok: false,
msg: "Incorrect username or password." msg: "Incorrect username or password.",
}) })
} }
@@ -154,23 +206,22 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Added Successfully." msg: "Added Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
// ***************************
// Auth Only API // Auth Only API
// ***************************
// Add a new monitor
socket.on("add", async (monitor, callback) => { socket.on("add", async (monitor, callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
@@ -191,17 +242,18 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Added Successfully.", msg: "Added Successfully.",
monitorID: bean.id monitorID: bean.id,
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
// Edit a monitor
socket.on("editMonitor", async (monitor, callback) => { socket.on("editMonitor", async (monitor, callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
@@ -217,8 +269,11 @@ let needSetup = false;
bean.url = monitor.url bean.url = monitor.url
bean.interval = monitor.interval bean.interval = monitor.interval
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries;
bean.port = monitor.port; bean.port = monitor.port;
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown;
await R.store(bean) await R.store(bean)
@@ -233,14 +288,14 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Saved.", msg: "Saved.",
monitorID: bean.id monitorID: bean.id,
}); });
} catch (e) { } catch (e) {
console.error(e) console.error(e)
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -264,7 +319,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -278,13 +333,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Resumed Successfully." msg: "Resumed Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -297,14 +352,13 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg: "Paused Successfully." msg: "Paused Successfully.",
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -322,12 +376,12 @@ let needSetup = false;
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
socket.userID socket.userID,
]); ]);
callback({ callback({
ok: true, ok: true,
msg: "Deleted Successfully." msg: "Deleted Successfully.",
}); });
await sendMonitorList(socket); await sendMonitorList(socket);
@@ -335,7 +389,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -349,19 +403,19 @@ let needSetup = false;
} }
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID socket.userID,
]) ])
if (user && passwordHash.verify(password.currentPassword, user.password)) { if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password.newPassword), passwordHash.generate(password.newPassword),
socket.userID socket.userID,
]); ]);
callback({ callback({
ok: true, ok: true,
msg: "Password has been updated successfully." msg: "Password has been updated successfully.",
}) })
} else { } else {
throw new Error("Incorrect current password") throw new Error("Incorrect current password")
@@ -370,25 +424,43 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
socket.on("getSettings", async (type, callback) => { socket.on("getSettings", async (callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
callback({ callback({
ok: true, ok: true,
data: await getSettings(type), data: await getSettings("general"),
}); });
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
});
}
});
socket.on("setSettings", async (data, callback) => {
try {
checkLogin(socket)
await setSettings("general", data)
callback({
ok: true,
msg: "Saved"
});
} catch (e) {
callback({
ok: false,
msg: e.message,
}); });
} }
}); });
@@ -409,7 +481,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -429,7 +501,7 @@ let needSetup = false;
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -442,7 +514,7 @@ let needSetup = false;
callback({ callback({
ok: true, ok: true,
msg msg,
}); });
} catch (e) { } catch (e) {
@@ -450,7 +522,7 @@ let needSetup = false;
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message,
}); });
} }
}); });
@@ -463,6 +535,18 @@ let needSetup = false;
callback(false); callback(false);
} }
}); });
debug("added all socket handlers")
debug("check auto login")
if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin")
await afterLogin(socket, await R.findOne("user"))
socket.emit("autoLogin")
} else {
debug("need auth")
}
}); });
console.log("Init") console.log("Init")
@@ -475,7 +559,7 @@ let needSetup = false;
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID monitorID,
]) ])
for (let notificationID in notificationIDList) { for (let notificationID in notificationIDList) {
@@ -508,7 +592,7 @@ async function sendMonitorList(socket) {
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
let result = []; let result = [];
let list = await R.find("notification", " user_id = ? ", [ let list = await R.find("notification", " user_id = ? ", [
socket.userID socket.userID,
]); ]);
for (let bean of list) { for (let bean of list) {
@@ -526,19 +610,19 @@ async function afterLogin(socket, user) {
let monitorList = await sendMonitorList(socket) let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) { for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID); sendHeartbeatList(socket, monitorID);
await sendImportantHeartbeatList(socket, monitorID); sendImportantHeartbeatList(socket, monitorID);
await Monitor.sendStats(io, monitorID, user.id) Monitor.sendStats(io, monitorID, user.id)
} }
await sendNotificationList(socket) sendNotificationList(socket)
} }
async function getMonitorJSONList(userID) { async function getMonitorJSONList(userID) {
let result = {}; let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [ let monitorList = await R.find("monitor", " user_id = ? ", [
userID userID,
]) ])
for (let monitor of monitorList) { for (let monitor of monitorList) {
@@ -555,24 +639,26 @@ function checkLogin(socket) {
} }
async function initDatabase() { async function initDatabase() {
const path = './data/kuma.db'; if (! fs.existsSync(Database.path)) {
if (! fs.existsSync(path)) {
console.log("Copying Database") console.log("Copying Database")
fs.copyFileSync("./db/kuma.db", path); fs.copyFileSync(Database.templatePath, Database.path);
} }
console.log("Connecting to Database") console.log("Connecting to Database")
R.setup('sqlite', { R.setup("sqlite", {
filename: path filename: Database.path,
}); });
console.log("Connected") console.log("Connected")
// Patch the database
await Database.patch()
// Auto map the model to a bean object
R.freeze(true) R.freeze(true)
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret" "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (! jwtSecretBean) {
@@ -587,6 +673,7 @@ async function initDatabase() {
console.log("Load JWT secret from database.") console.log("Load JWT secret from database.")
} }
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
if ((await R.count("user")) === 0) { if ((await R.count("user")) === 0) {
console.log("No user, need setup") console.log("No user, need setup")
needSetup = true; needSetup = true;
@@ -602,11 +689,11 @@ async function startMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
userID userID,
]); ]);
let monitor = await R.findOne("monitor", " id = ? ", [ let monitor = await R.findOne("monitor", " id = ? ", [
monitorID monitorID,
]) ])
if (monitor.id in monitorList) { if (monitor.id in monitorList) {
@@ -628,7 +715,7 @@ async function pauseMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
monitorID, monitorID,
userID userID,
]); ]);
if (monitorID in monitorList) { if (monitorID in monitorList) {
@@ -657,13 +744,13 @@ async function sendHeartbeatList(socket, monitorID) {
ORDER BY time DESC ORDER BY time DESC
LIMIT 100 LIMIT 100
`, [ `, [
monitorID monitorID,
]) ])
let result = []; let result = [];
for (let bean of list) { for (let bean of list) {
result.unshift(bean.toJSON()) result.unshift(bean.toJSON())
} }
socket.emit("heartbeatList", monitorID, result) socket.emit("heartbeatList", monitorID, result)
@@ -676,77 +763,35 @@ async function sendImportantHeartbeatList(socket, monitorID) {
ORDER BY time DESC ORDER BY time DESC
LIMIT 500 LIMIT 500
`, [ `, [
monitorID monitorID,
]) ])
socket.emit("importantHeartbeatList", monitorID, list) socket.emit("importantHeartbeatList", monitorID, list)
} }
const startGracefulShutdown = async () => {
console.log('Shutdown requested');
await (new Promise((resolve) => {
server.close(async function () {
console.log('Stopped Express.');
process.exit(0)
setTimeout(async () =>{
await R.close();
console.log("Stopped DB")
resolve();
}, 5000)
});
}));
}
let noReject = true;
process.on('unhandledRejection', (reason, p) => {
noReject = false;
});
async function shutdownFunction(signal) { async function shutdownFunction(signal) {
console.log('Called signal: ' + signal); console.log("Shutdown requested");
console.log("Called signal: " + signal);
console.log("Stopping all monitors") console.log("Stopping all monitors")
for (let id in monitorList) { for (let id in monitorList) {
let monitor = monitorList[id] let monitor = monitorList[id]
monitor.stop() monitor.stop()
} }
await sleep(2000) await sleep(2000);
await Database.close();
console.log("Closing DB") console.log("Stopped DB")
// Special handle, because tarn.js throw a promise reject that cannot be caught
while (true) {
noReject = true;
await R.close()
await sleep(2000)
if (noReject) {
break;
} else {
console.log("Waiting...")
}
}
console.log("OK")
} }
function finalFunction() { function finalFunction() {
console.log('Graceful Shutdown') console.log("Graceful Shutdown Done")
} }
gracefulShutdown(server, { gracefulShutdown(server, {
signals: 'SIGINT SIGTERM', signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode development: false, // not in dev mode
forceExit: true, // triggers process.exit() at the end of shutdown process forceExit: true, // triggers process.exit() at the end of shutdown process
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ... onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction // finally function (sync) - e.g. for logging finally: finalFunction, // finally function (sync) - e.g. for logging
}); });

View File

@@ -1,6 +1,7 @@
const tcpp = require('tcp-ping'); const tcpp = require("tcp-ping");
const Ping = require("./ping-lite"); const Ping = require("./ping-lite");
const {R} = require("redbean-node"); const { R } = require("redbean-node");
const { debug } = require("../src/util");
exports.tcping = function (hostname, port) { exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -40,21 +41,120 @@ exports.ping = function (hostname) {
} }
exports.setting = async function (key) { exports.setting = async function (key) {
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key key,
]);
try {
const v = JSON.parse(value);
debug(`Get Setting: ${key}: ${v}`)
return v;
} catch (e) {
return value;
}
}
exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
]) ])
if (! bean) {
bean = R.dispense("setting")
bean.key = key;
}
bean.value = JSON.stringify(value);
await R.store(bean)
} }
exports.getSettings = async function (type) { exports.getSettings = async function (type) {
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [ let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type type,
]) ])
let result = {}; let result = {};
for (let row of list) { for (let row of list) {
result[row.key] = row.value; try {
result[row.key] = JSON.parse(row.value);
} catch (e) {
result[row.key] = row.value;
}
} }
return result; return result;
} }
exports.setSettings = async function (type, data) {
let keyList = Object.keys(data);
let promiseList = [];
for (let key of keyList) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
]);
if (bean == null) {
bean = R.dispense("setting");
bean.type = type;
bean.key = key;
}
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
promiseList.push(R.store(bean))
}
}
await Promise.all(promiseList);
}
// ssl-checker by @dyaa
// param: res - response object from axios
// return an object containing the certificate information
const getDaysBetween = (validFrom, validTo) =>
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
const getDaysRemaining = (validFrom, validTo) => {
const daysRemaining = getDaysBetween(validFrom, validTo);
if (new Date(validTo).getTime() < new Date().getTime()) {
return -daysRemaining;
}
return daysRemaining;
};
exports.checkCertificate = function (res) {
const {
valid_from,
valid_to,
subjectaltname,
issuer,
fingerprint,
} = res.request.res.socket.getPeerCertificate(false);
if (!valid_from || !valid_to || !subjectaltname) {
throw {
message: "No TLS certificate in response",
};
}
const valid = res.request.res.socket.authorized || false;
const validTo = new Date(valid_to);
const validFor = subjectaltname
.replace(/DNS:|IP Address:/g, "")
.split(", ");
const daysRemaining = getDaysRemaining(new Date(), validTo);
return {
valid,
validFor,
validTo,
daysRemaining,
issuer,
fingerprint,
};
}

View File

@@ -1,16 +0,0 @@
// Common JS cannot be used in frontend sadly
// sleep, ucfirst is duplicated in ../src/util-frontend.js
exports.sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
exports.ucfirst = function (str) {
if (! str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}

View File

@@ -3,11 +3,5 @@
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>
<style lang="scss">
</style>

View File

@@ -1,7 +1,8 @@
$primary: #5CDD8B; $primary: #5CDD8B;
$danger: #DC3545; $danger: #DC3545;
$warning: #f8a306;
$link-color: #111; $link-color: #111;
$border-radius: 50rem; $border-radius: 50rem;
$highlight: #7ce8a4; $highlight: #7ce8a4;
$highlight-white: #e7faec; $highlight-white: #e7faec;

View File

@@ -1,17 +1,23 @@
<template> <template>
<div class="modal fade" tabindex="-1" ref="modal"> <div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Confirm</h5> <h5 id="exampleModalLabel" class="modal-title">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> Confirm
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot></slot> <slot />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button> <button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> {{ yesText }}
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ noText }}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -19,17 +25,25 @@
</template> </template>
<script> <script>
import { Modal } from 'bootstrap' import { Modal } from "bootstrap"
export default { export default {
props: { props: {
btnStyle: { btnStyle: {
type: String, type: String,
default: "btn-primary" default: "btn-primary",
} },
yesText: {
type: String,
default: "Yes",
},
noText: {
type: String,
default: "No",
},
}, },
data: () => ({ data: () => ({
modal: null modal: null,
}), }),
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal)
@@ -39,12 +53,8 @@ export default {
this.modal.show() this.modal.show()
}, },
yes() { yes() {
this.$emit('yes'); this.$emit("yes");
} },
} },
} }
</script> </script>
<style scoped>
</style>

View File

@@ -3,26 +3,22 @@
<span v-else>{{ value }}</span> <span v-else>{{ value }}</span>
</template> </template>
<script> <script lang="ts">
import {sleep} from '../util-frontend' import { sleep } from "../util.ts"
export default { export default {
props: { props: {
value: [String, Number], value: [String, Number],
time: { time: {
Number, type: Number,
default: 0.3, default: 0.3,
}, },
unit: { unit: {
String, type: String,
default: "ms", default: "ms",
} },
},
mounted() {
this.output = this.value;
}, },
data() { data() {
@@ -32,14 +28,10 @@ export default {
} }
}, },
methods: {
},
computed: { computed: {
isNum() { isNum() {
return typeof this.value === 'number' return typeof this.value === "number"
} },
}, },
watch: { watch: {
@@ -61,9 +53,11 @@ export default {
}, },
}, },
mounted() {
this.output = this.value;
},
methods: {},
} }
</script> </script>
<style scoped>
</style>

View File

@@ -4,9 +4,9 @@
<script> <script>
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import utc from 'dayjs/plugin/utc' import utc from "dayjs/plugin/utc"
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@@ -14,17 +14,24 @@ dayjs.extend(relativeTime)
export default { export default {
props: { props: {
value: String, value: String,
dateOnly: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
displayText() { displayText() {
let format = "YYYY-MM-DD HH:mm:ss"; if (this.value !== undefined && this.value !== "") {
return dayjs.utc(this.value).tz(this.$root.timezone).format(format) let format = "YYYY-MM-DD HH:mm:ss";
if (this.dateOnly) {
format = "YYYY-MM-DD";
}
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
}
return "";
}, },
} },
} }
</script> </script>
<style scoped>
</style>

View File

@@ -1,28 +1,27 @@
<template> <template>
<div class="wrap" :style="wrapStyle" ref="wrap"> <div ref="wrap" class="wrap" :style="wrapStyle">
<div class="hp-bar-big" :style="barStyle"> <div class="hp-bar-big" :style="barStyle">
<div <div
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
:style="beatStyle"
v-for="(beat, index) in shortBeatList" v-for="(beat, index) in shortBeatList"
:key="index" :key="index"
:title="beat.msg"> class="beat"
</div> :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:style="beatStyle"
:title="beat.msg"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
size: { size: {
type: String, type: String,
default: "big" default: "big",
}, },
monitorId: Number monitorId: Number,
}, },
data() { data() {
return { return {
@@ -34,26 +33,6 @@ export default {
maxBeat: -1, maxBeat: -1,
} }
}, },
unmounted() {
window.removeEventListener("resize", this.resize);
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
}
window.addEventListener("resize", this.resize);
this.resize();
},
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
}
},
computed: { computed: {
beatList() { beatList() {
@@ -80,8 +59,6 @@ export default {
start = 0; start = 0;
} }
return placeholders.concat(this.beatList.slice(start)) return placeholders.concat(this.beatList.slice(start))
}, },
@@ -89,16 +66,9 @@ export default {
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2); let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2); let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
let width
if (this.maxBeat > 0) {
width = (this.beatWidth + this.beatMargin * 2) * this.maxBeat + (leftRight * 2) + "px"
} {
width = "100%"
}
return { return {
padding: `${topBottom}px ${leftRight}px`, padding: `${topBottom}px ${leftRight}px`,
width: width width: "100%",
} }
}, },
@@ -111,11 +81,11 @@ export default {
transform: `translateX(${width}px)`, transform: `translateX(${width}px)`,
} }
} else {
return {
transform: `translateX(0)`,
}
} }
return {
transform: "translateX(0)",
}
}, },
beatStyle() { beatStyle() {
@@ -125,7 +95,7 @@ export default {
margin: this.beatMargin + "px", margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale, "--hover-scale": this.hoverScale,
} }
} },
}, },
watch: { watch: {
@@ -138,8 +108,28 @@ export default {
}, 300) }, 300)
}, },
deep: true, deep: true,
},
},
unmounted() {
window.removeEventListener("resize", this.resize);
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
} }
}
window.addEventListener("resize", this.resize);
this.resize();
},
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
},
},
} }
</script> </script>
@@ -166,6 +156,10 @@ export default {
background-color: $danger; background-color: $danger;
} }
&.pending {
background-color: $warning;
}
&:not(.empty):hover { &:not(.empty):hover {
transition: all ease-in-out 0.15s; transition: all ease-in-out 0.15s;
opacity: 0.8; opacity: 0.8;

View File

@@ -2,31 +2,32 @@
<div class="form-container"> <div class="form-container">
<div class="form"> <div class="form">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal" />
<h1 class="h3 mb-3 fw-normal"></h1>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username"> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">Username</label> <label for="floatingInput">Username</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password"> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">Password</label> <label for="floatingPassword">Password</label>
</div> </div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4"> <div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check"> <div class="form-check">
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember"> <input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember"> <label class="form-check-label" for="remember">
Remember me Remember me
</label> </label>
</div> </div>
</div> </div>
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button> <button class="w-100 btn btn-primary" type="submit" :disabled="processing">
Login
</button>
<div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok"> <div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
{{ res.msg }} {{ res.msg }}
</div> </div>
</form> </form>
@@ -52,8 +53,8 @@ export default {
this.processing = false; this.processing = false;
this.res = res; this.res = res;
}) })
} },
} },
} }
</script> </script>

View File

@@ -1,18 +1,18 @@
<template> <template>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5> <h5 id="exampleModalLabel" class="modal-title">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> Setup Notification
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">Notification Type</label> <label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="notification.type"> <select id="type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option> <option value="telegram">Telegram</option>
<option value="webhook">Webhook</option> <option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option> <option value="smtp">Email (SMTP)</option>
@@ -21,28 +21,34 @@
<option value="gotify">Gotify</option> <option value="gotify">Gotify</option>
<option value="slack">Slack</option> <option value="slack">Slack</option>
<option value="pushover">Pushover</option> <option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option> <option value="apprise">Apprise (Support 50+ Notification services)</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="notification.name"> <input id="name" v-model="notification.name" type="text" class="form-control" required>
</div> </div>
<template v-if="notification.type === 'telegram'"> <template v-if="notification.type === 'telegram'">
<div class="mb-3"> <div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label> <label for="telegram-bot-token" class="form-label">Bot Token</label>
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken"> <input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
<div class="form-text">You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.</div> <div class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label> <label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID"> <input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button> <button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
Auto Get
</button>
</div> </div>
<div class="form-text"> <div class="form-text">
@@ -53,9 +59,8 @@
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken"> <template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a> <a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template> </template>
<template v-else> <template v-else>
@@ -69,15 +74,18 @@
<template v-if="notification.type === 'webhook'"> <template v-if="notification.type === 'webhook'">
<div class="mb-3"> <div class="mb-3">
<label for="webhook-url" class="form-label">Post URL</label> <label for="webhook-url" class="form-label">Post URL</label>
<input type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL"> <input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="webhook-content-type" class="form-label">Content Type</label> <label for="webhook-content-type" class="form-label">Content Type</label>
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required> <select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
<option value="json">application/json</option> <option value="json">
<option value="form-data">multipart/form-data</option> application/json
</option>
<option value="form-data">
multipart/form-data
</option>
</select> </select>
<div class="form-text"> <div class="form-text">
@@ -90,70 +98,71 @@
<template v-if="notification.type === 'smtp'"> <template v-if="notification.type === 'smtp'">
<div class="mb-3"> <div class="mb-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" required v-model="notification.smtpHost"> <input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input type="number" class="form-control" id="port" v-model="notification.smtpPort" required min="0" max="65535" step="1"> <input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure"> <input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="secure"> <label class="form-check-label" for="secure">
Secure Secure
</label> </label>
</div> </div>
<div class="form-text">Generally, true for 465, false for other ports.</div> <div class="form-text">
Generally, true for 465, false for other ports.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Username</label> <label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" v-model="notification.smtpUsername" autocomplete="false"> <input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false"> <input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="from-email" class="form-label">From Email</label> <label for="from-email" class="form-label">From Email</label>
<input type="email" class="form-control" id="from-email" required v-model="notification.smtpFrom" autocomplete="false"> <input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="to-email" class="form-label">To Email</label> <label for="to-email" class="form-label">To Email</label>
<input type="email" class="form-control" id="to-email" required v-model="notification.smtpTo" autocomplete="false"> <input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
</div> </div>
</template> </template>
<template v-if="notification.type === 'discord'"> <template v-if="notification.type === 'discord'">
<div class="mb-3"> <div class="mb-3">
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label> <label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
<input type="text" class="form-control" id="discord-webhook-url" required v-model="notification.discordWebhookUrl" autocomplete="false"> <input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
<div class="form-text">You can get this by going to Server Settings -> Integrations -> Create Webhook</div> <div class="form-text">
You can get this by going to Server Settings -> Integrations -> Create Webhook
</div>
</div> </div>
</template> </template>
<template v-if="notification.type === 'signal'"> <template v-if="notification.type === 'signal'">
<div class="mb-3"> <div class="mb-3">
<label for="signal-url" class="form-label">Post URL</label> <label for="signal-url" class="form-label">Post URL</label>
<input type="url" pattern="https?://.+" class="form-control" id="signal-url" required v-model="notification.signalURL"> <input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="signal-number" class="form-label">Number</label> <label for="signal-number" class="form-label">Number</label>
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber"> <input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="signal-recipients" class="form-label">Recipients</label> <label for="signal-recipients" class="form-label">Recipients</label>
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients"> <input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
You need to have a signal client with REST API. You need to have a signal client with REST API.
@@ -174,37 +183,37 @@
</template> </template>
<template v-if="notification.type === 'gotify'"> <template v-if="notification.type === 'gotify'">
<div class="mb-3"> <div class="mb-3">
<label for="gotify-application-token" class="form-label">Application Token</label> <label for="gotify-application-token" class="form-label">Application Token</label>
<input type="text" class="form-control" id="gotify-application-token" required v-model="notification.gotifyapplicationToken"> <input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="gotify-server-url" class="form-label">Server URL</label> <label for="gotify-server-url" class="form-label">Server URL</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl"> <input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
</div>
</div> </div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="gotify-priority" class="form-label">Priority</label> <label for="gotify-priority" class="form-label">Priority</label>
<input type="number" class="form-control" id="gotify-priority" v-model="notification.gotifyPriority" required min="0" max="10" step="1"> <input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
</div> </div>
</template> </template>
<template v-if="notification.type === 'slack'"> <template v-if="notification.type === 'slack'">
<div class="mb-3"> <div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label> <label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input type="text" class="form-control" id="slack-webhook-url" required v-model="notification.slackwebhookURL"> <input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label> <label for="slack-username" class="form-label">Username</label>
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername"> <input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">Icon Emoji</label> <label for="slack-iconemo" class="form-label">Icon Emoji</label>
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo"> <input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">Channel Name</label> <label for="slack-channel" class="form-label">Channel Name</label>
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel"> <input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label> <label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
<input type="text" class="form-control" id="slack-button" v-model="notification.slackbutton"> <input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text"> <div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required <span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a> More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p> </p>
@@ -221,20 +230,43 @@
</div> </div>
</template> </template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input type="text" class="form-control" id="pushy-app-token" required v-model="notification.pushyAPIKey">
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="pushy-user-key" required v-model="notification.pushyToken">
</div>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</p>
</template>
<template v-if="notification.type === 'pushover'"> <template v-if="notification.type === 'pushover'">
<div class="mb-3"> <div class="mb-3">
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
<input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label> <label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
<input type="text" class="form-control" id="pushover-user" required v-model="notification.pushoveruserkey"> <input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
<label for="pushover-device" class="form-label">Device</label> <label for="pushover-device" class="form-label">Device</label>
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice"> <input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label> <label for="pushover-device" class="form-label">Message Title</label>
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle"> <input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
<label for="pushover-priority" class="form-label">Priority</label> <label for="pushover-priority" class="form-label">Priority</label>
<input type="text" class="form-control" id="pushover-priority" v-model="notification.pushoverpriority"> <select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
<option>-2</option>
<option>-1</option>
<option>0</option>
<option>1</option>
<option>2</option>
</select>
<label for="pushover-sound" class="form-label">Notification Sound</label> <label for="pushover-sound" class="form-label">Notification Sound</label>
<select class="form-select" id="pushover-sound" v-model="notification.pushoversounds"> <select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
<option>pushover</option> <option>pushover</option>
<option>bike</option> <option>bike</option>
<option>bugle</option> <option>bugle</option>
@@ -259,22 +291,24 @@
<option>none</option> <option>none</option>
</select> </select>
<div class="form-text"> <div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required <span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a> More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p> </p>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
Emergency priority(2) has default 30 second timeout between retries and will expire after 1 hour. Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
</p> </p>
<p style="margin-top: 8px;">
If you want to send notifications to different devices, fill out Device field.
</p>
</div> </div>
</div> </div>
</template> </template>
<template v-if="notification.type === 'apprise'"> <template v-if="notification.type === 'apprise'">
<div class="mb-3"> <div class="mb-3">
<label for="gotify-application-token" class="form-label">Apprise URL</label> <label for="apprise-url" class="form-label">Apprise URL</label>
<input type="text" class="form-control" id="gotify-application-token" required v-model="notification.appriseURL"> <input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p> <p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
<p> <p>
@@ -282,45 +316,60 @@
</p> </p>
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<p> <p>
Status: Status:
<span class="text-primary" v-if="appriseInstalled">Apprise is installed</span> <span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
<span class="text-danger" v-else>Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span> <span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
</p> </p>
</div> </div>
</template>
<template v-if="notification.type === 'lunasea'">
<div class="mb-3">
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color:red;"><sup>*</sup></span>Required</p>
</div>
</div>
</template> </template>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button> <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button> Delete
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button> </button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
Test
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
Save
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
<Confirm ref="confirmDelete" @yes="deleteNotification" btn-style="btn-danger">Are you sure want to delete this notification for all monitors?</Confirm> <Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
Are you sure want to delete this notification for all monitors?
</Confirm>
</template> </template>
<script> <script lang="ts">
import { Modal } from 'bootstrap' import { Modal } from "bootstrap"
import { ucfirst } from '../util-frontend' import { ucfirst } from "../util.ts"
import axios from "axios"; import axios from "axios";
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
import Confirm from "./Confirm.vue"; import Confirm from "./Confirm.vue";
const toast = useToast() const toast = useToast()
export default { export default {
components: {Confirm}, components: {
props: { Confirm,
}, },
props: {},
data() { data() {
return { return {
model: null, model: null,
@@ -329,11 +378,37 @@ export default {
notification: { notification: {
name: "", name: "",
type: null, type: null,
gotifyPriority: 8 gotifyPriority: 8,
}, },
appriseInstalled: false, appriseInstalled: false,
} }
}, },
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Alert (1)`;
} else {
oldName = "";
}
if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)`
}
},
},
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal) this.modal = new Modal(this.$refs.modal)
@@ -431,35 +506,5 @@ export default {
}, },
}, },
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Alert (1)`;
} else {
oldName = "";
}
if (! this.notification.name || this.notification.name === oldName) {
this.notification.name = `My ${ucfirst(to)} Alert (1)`
}
}
}
} }
</script> </script>
<style scoped>
</style>

View File

@@ -5,35 +5,47 @@
<script> <script>
export default { export default {
props: { props: {
status: Number status: Number,
}, },
computed: { computed: {
color() { color() {
if (this.status === 0) { if (this.status === 0) {
return "danger" return "danger"
} else if (this.status === 1) {
return "primary"
} else {
return "secondary"
} }
if (this.status === 1) {
return "primary"
}
if (this.status === 2) {
return "warning"
}
return "secondary"
}, },
text() { text() {
if (this.status === 0) { if (this.status === 0) {
return "Down" return "Down"
} else if (this.status === 1) {
return "Up"
} else {
return "Unknown"
} }
if (this.status === 1) {
return "Up"
}
if (this.status === 2) {
return "Pending"
}
return "Unknown"
}, },
} },
} }
</script> </script>
<style scoped> <style scoped>
span { span {
width: 45px; width: 64px;
} }
</style> </style>

View File

@@ -5,10 +5,10 @@
<script> <script>
export default { export default {
props: { props: {
monitor : Object, monitor: Object,
type: String, type: String,
pill: { pill: {
Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
@@ -20,42 +20,50 @@ export default {
if (this.$root.uptimeList[key] !== undefined) { if (this.$root.uptimeList[key] !== undefined) {
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%"; return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
} else {
return "N/A"
} }
return "N/A"
}, },
color() { color() {
if (this.lastHeartBeat.status === 0) { if (this.lastHeartBeat.status === 0) {
return "danger" return "danger"
} else if (this.lastHeartBeat.status === 1) {
return "primary"
} else {
return "secondary"
} }
if (this.lastHeartBeat.status === 1) {
return "primary"
}
if (this.lastHeartBeat.status === 2) {
return "warning"
}
return "secondary"
}, },
lastHeartBeat() { lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id] return this.$root.lastHeartbeatList[this.monitor.id]
} else { }
return { status: -1 }
return {
status: -1,
} }
}, },
className() { className() {
if (this.pill) { if (this.pill) {
return `badge rounded-pill bg-${this.color}`; return `badge rounded-pill bg-${this.color}`;
} else {
return "";
} }
return "";
}, },
}, },
} }
</script> </script>
<style scoped> <style>
.badge {
min-width: 62px;
}
</style> </style>

10
src/icon.js Normal file
View File

@@ -0,0 +1,10 @@
import { library } from "@fortawesome/fontawesome-svg-core"
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
//import { fa } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
// Add Free Font Awesome Icons here
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
export { FontAwesomeIcon }

View File

@@ -3,11 +3,5 @@
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>
<style scoped>
</style>

View File

@@ -1,28 +1,35 @@
<template> <template>
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
<div class="container-fluid"> <div class="container-fluid">
Lost connection to the socket server. Reconnecting... {{ $root.connectionErrorMsg }}
</div> </div>
</div> </div>
<!-- Desktop header --> <!-- Desktop header -->
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile"> <header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> <router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo"></object> <object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
<span class="fs-4 title">Uptime Kuma</span> <span class="fs-4 title">Uptime Kuma</span>
</router-link> </router-link>
<ul class="nav nav-pills" > <ul class="nav nav-pills">
<li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li> <li class="nav-item">
<li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li> <router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> Dashboard
</router-link>
</li>
<li class="nav-item">
<router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> Settings
</router-link>
</li>
</ul> </ul>
</header> </header>
<!-- Mobile header --> <!-- Mobile header -->
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else> <header v-else class="d-flex flex-wrap justify-content-center mt-3 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none"> <router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg"></object> <object class="bi" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span> <span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link> </router-link>
</header> </header>
@@ -42,12 +49,27 @@
</footer> </footer>
<!-- Mobile Only --> <!-- Mobile Only -->
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div> <div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
<nav class="bottom-nav" v-if="$root.isMobile"> <nav v-if="$root.isMobile" class="bottom-nav">
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link> <router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a> <div><font-awesome-icon icon="tachometer-alt" /></div>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div></div>Add</router-link> Dashboard
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link> </router-link>
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile">
<div><font-awesome-icon icon="list" /></div>
List
</a>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="plus" /></div>
Add
</router-link>
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="cog" /></div>
Settings
</router-link>
</nav> </nav>
</template> </template>
@@ -56,23 +78,19 @@ import Login from "../components/Login.vue";
export default { export default {
components: { components: {
Login Login,
}, },
data() { data() {
return { return {}
}
},
computed: {
},
mounted() {
this.init();
}, },
computed: {},
watch: { watch: {
$route (to, from) { $route (to, from) {
this.init(); this.init();
} },
},
mounted() {
this.init();
}, },
methods: { methods: {
init() { init() {
@@ -81,7 +99,7 @@ export default {
} }
}, },
} },
} }
</script> </script>
@@ -99,7 +117,7 @@ export default {
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05);
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
padding: 0 35px; padding: 0 10px;
a { a {
text-align: center; text-align: center;
@@ -137,13 +155,10 @@ export default {
color: white; color: white;
} }
main {
}
footer { footer {
color: #AAA; color: #AAA;
font-size: 13px; font-size: 13px;
margin-top: 10px;
margin-bottom: 30px; margin-bottom: 30px;
margin-left: 10px; margin-left: 10px;
text-align: center; text-align: center;

View File

@@ -1,58 +1,58 @@
import {createApp, h} from "vue"; import "bootstrap";
import {createRouter, createWebHistory} from 'vue-router' import { createApp, h } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from './App.vue' import Toast from "vue-toastification";
import Layout from './layouts/Layout.vue' import "vue-toastification/dist/index.css";
import EmptyLayout from './layouts/EmptyLayout.vue' import App from "./App.vue";
import Settings from "./pages/Settings.vue"; import "./assets/app.scss";
import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue";
import socket from "./mixins/socket";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue"; import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue"; import Details from "./pages/Details.vue";
import socket from "./mixins/socket"
import "./assets/app.scss"
import EditMonitor from "./pages/EditMonitor.vue"; import EditMonitor from "./pages/EditMonitor.vue";
import Toast from "vue-toastification"; import Settings from "./pages/Settings.vue";
import "vue-toastification/dist/index.css";
import "bootstrap"
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
const routes = [ const routes = [
{ {
path: '/', path: "/",
component: Layout, component: Layout,
children: [ children: [
{ {
name: "root", name: "root",
path: '', path: "",
component: Dashboard, component: Dashboard,
children: [ children: [
{ {
name: "DashboardHome", name: "DashboardHome",
path: '/dashboard', path: "/dashboard",
component: DashboardHome, component: DashboardHome,
children: [ children: [
{ {
path: '/dashboard/:id', path: "/dashboard/:id",
component: EmptyLayout, component: EmptyLayout,
children: [ children: [
{ {
path: '', path: "",
component: Details, component: Details,
}, },
{ {
path: '/edit/:id', path: "/edit/:id",
component: EditMonitor, component: EditMonitor,
}, },
] ],
}, },
{ {
path: '/add', path: "/add",
component: EditMonitor, component: EditMonitor,
}, },
] ],
}, },
{ {
path: '/settings', path: "/settings",
component: Settings, component: Settings,
}, },
], ],
@@ -62,13 +62,13 @@ const routes = [
}, },
{ {
path: '/setup', path: "/setup",
component: Setup, component: Setup,
}, },
] ]
const router = createRouter({ const router = createRouter({
linkActiveClass: 'active', linkActiveClass: "active",
history: createWebHistory(), history: createWebHistory(),
routes, routes,
}) })
@@ -77,16 +77,17 @@ const app = createApp({
mixins: [ mixins: [
socket, socket,
], ],
render: ()=>h(App) render: () => h(App),
}) })
app.use(router) app.use(router)
const options = { const options = {
position: "bottom-right" position: "bottom-right",
}; };
app.use(Toast, options); app.use(Toast, options);
app.mount('#app') app.component("FontAwesomeIcon", FontAwesomeIcon)
app.mount("#app")

View File

@@ -1,6 +1,6 @@
import {io} from "socket.io-client";
import { useToast } from 'vue-toastification'
import dayjs from "dayjs"; import dayjs from "dayjs";
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast()
let socket; let socket;
@@ -25,14 +25,16 @@ export default {
importantHeartbeatList: { }, importantHeartbeatList: { },
avgPingList: { }, avgPingList: { },
uptimeList: { }, uptimeList: { },
certInfoList: {},
notificationList: [], notificationList: [],
windowWidth: window.innerWidth, windowWidth: window.innerWidth,
showListMobile: false, showListMobile: false,
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
} }
}, },
created() { created() {
window.addEventListener('resize', this.onResize); window.addEventListener("resize", this.onResize);
let wsHost; let wsHost;
const env = process.env.NODE_ENV || "production"; const env = process.env.NODE_ENV || "production";
@@ -43,30 +45,42 @@ export default {
} }
socket = io(wsHost, { socket = io(wsHost, {
transports: ['websocket'] transports: ["websocket"],
}); });
socket.on("connect_error", (err) => { socket.on("info", (info) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
});
socket.on('info', (info) => {
this.info = info; this.info = info;
}); });
socket.on('setup', (monitorID, data) => { socket.on("setup", (monitorID, data) => {
this.$router.push("/setup") this.$router.push("/setup")
}); });
socket.on('monitorList', (data) => { socket.on("autoLogin", (monitorID, data) => {
this.loggedIn = true;
this.storage().token = "autoLogin";
this.allowLoginDialog = false;
});
socket.on("monitorList", (data) => {
// Add Helper function
Object.entries(data).forEach(([monitorID, monitor]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
} catch (_) {
return null;
}
};
});
this.monitorList = data; this.monitorList = data;
}); });
socket.on('notificationList', (data) => { socket.on("notificationList", (data) => {
this.notificationList = data; this.notificationList = data;
}); });
socket.on('heartbeat', (data) => { socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) { if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = []; this.heartbeatList[data.monitorID] = [];
} }
@@ -89,7 +103,6 @@ export default {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
} }
if (! (data.monitorID in this.importantHeartbeatList)) { if (! (data.monitorID in this.importantHeartbeatList)) {
this.importantHeartbeatList[data.monitorID] = []; this.importantHeartbeatList[data.monitorID] = [];
} }
@@ -98,7 +111,7 @@ export default {
} }
}); });
socket.on('heartbeatList', (monitorID, data) => { socket.on("heartbeatList", (monitorID, data) => {
if (! (monitorID in this.heartbeatList)) { if (! (monitorID in this.heartbeatList)) {
this.heartbeatList[monitorID] = data; this.heartbeatList[monitorID] = data;
} else { } else {
@@ -106,15 +119,19 @@ export default {
} }
}); });
socket.on('avgPing', (monitorID, data) => { socket.on("avgPing", (monitorID, data) => {
this.avgPingList[monitorID] = data this.avgPingList[monitorID] = data
}); });
socket.on('uptime', (monitorID, type, data) => { socket.on("uptime", (monitorID, type, data) => {
this.uptimeList[`${monitorID}_${type}`] = data this.uptimeList[`${monitorID}_${type}`] = data
}); });
socket.on('importantHeartbeatList', (monitorID, data) => { socket.on("certInfo", (monitorID, data) => {
this.certInfoList[monitorID] = JSON.parse(data)
});
socket.on("importantHeartbeatList", (monitorID, data) => {
if (! (monitorID in this.importantHeartbeatList)) { if (! (monitorID in this.importantHeartbeatList)) {
this.importantHeartbeatList[monitorID] = data; this.importantHeartbeatList[monitorID] = data;
} else { } else {
@@ -122,12 +139,20 @@ export default {
} }
}); });
socket.on('disconnect', () => { socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
this.socket.connected = false;
this.socket.firstConnect = false;
});
socket.on("disconnect", () => {
console.log("disconnect") console.log("disconnect")
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
this.socket.connected = false; this.socket.connected = false;
}); });
socket.on('connect', () => { socket.on("connect", () => {
console.log("connect") console.log("connect")
this.socket.connectCount++; this.socket.connectCount++;
this.socket.connected = true; this.socket.connected = true;
@@ -137,8 +162,22 @@ export default {
this.clearData() this.clearData()
} }
if (this.storage().token) { let token = this.storage().token;
this.loginByToken(this.storage().token)
if (token) {
if (token !== "autoLogin") {
this.loginByToken(token)
} else {
// Timeout if it is not actually auto login
setTimeout(() => {
if (! this.loggedIn) {
this.allowLoginDialog = true;
this.$root.storage().removeItem("token");
}
}, 5000);
}
} else { } else {
this.allowLoginDialog = true; this.allowLoginDialog = true;
} }
@@ -186,7 +225,7 @@ export default {
this.loggedIn = true; this.loggedIn = true;
// Trigger Chrome Save Password // Trigger Chrome Save Password
history.pushState({}, '') history.pushState({}, "")
} }
callback(res) callback(res)
@@ -239,10 +278,9 @@ export default {
if (this.userTimezone === "auto") { if (this.userTimezone === "auto") {
return dayjs.tz.guess() return dayjs.tz.guess()
} else {
return this.userTimezone
} }
return this.userTimezone
}, },
lastHeartbeatList() { lastHeartbeatList() {
@@ -261,7 +299,7 @@ export default {
let unknown = { let unknown = {
text: "Unknown", text: "Unknown",
color: "secondary" color: "secondary",
} }
for (let monitorID in this.lastHeartbeatList) { for (let monitorID in this.lastHeartbeatList) {
@@ -272,12 +310,17 @@ export default {
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === 1) {
result[monitorID] = { result[monitorID] = {
text: "Up", text: "Up",
color: "primary" color: "primary",
}; };
} else if (lastHeartBeat.status === 0) { } else if (lastHeartBeat.status === 0) {
result[monitorID] = { result[monitorID] = {
text: "Down", text: "Down",
color: "danger" color: "danger",
};
} else if (lastHeartBeat.status === 2) {
result[monitorID] = {
text: "Pending",
color: "warning",
}; };
} else { } else {
result[monitorID] = unknown; result[monitorID] = unknown;
@@ -285,23 +328,22 @@ export default {
} }
return result; return result;
} },
}, },
watch: { watch: {
// Reload the SPA if the server version is changed. // Reload the SPA if the server version is changed.
"info.version"(to, from) { "info.version"(to, from) {
if (from && from !== to) { if (from && from !== to) {
window.location.reload() window.location.reload()
} }
}, },
remember() { remember() {
localStorage.remember = (this.remember) ? "1" : "0" localStorage.remember = (this.remember) ? "1" : "0"
} },
} },
} }

View File

@@ -1,36 +1,29 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-xl-4"> <div class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile"> <div v-if="! $root.isMobile">
<router-link to="/add" class="btn btn-primary">Add New Monitor</router-link> <router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
</div> </div>
<div class="shadow-box list mb-4" v-if="showList"> <div v-if="showList" class="shadow-box list mb-4">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
<div class="text-center mt-3" v-if="Object.keys($root.monitorList).length === 0">
No Monitors, please <router-link to="/add">add one</router-link>. No Monitors, please <router-link to="/add">add one</router-link>.
</div> </div>
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in sortedMonitorList" @click="$root.cancelActiveList"> <router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" @click="$root.cancelActiveList">
<div class="row"> <div class="row">
<div class="col-6 col-md-8 small-padding"> <div class="col-6 col-md-8 small-padding">
<div class="info"> <div class="info">
<Uptime :monitor="item" type="24" :pill="true" /> <Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }} {{ item.name }}
</div> </div>
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" /> <HeartbeatBar size="small" :monitor-id="item.id" />
</div> </div>
</div> </div>
</router-link> </router-link>
</div> </div>
</div> </div>
<div class="col-12 col-md-7 col-xl-8"> <div class="col-12 col-md-7 col-xl-8">
@@ -38,7 +31,6 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@@ -49,12 +41,10 @@ import Uptime from "../components/Uptime.vue";
export default { export default {
components: { components: {
Uptime, Uptime,
HeartbeatBar HeartbeatBar,
}, },
data() { data() {
return { return {}
}
}, },
computed: { computed: {
sortedMonitorList() { sortedMonitorList() {
@@ -94,8 +84,8 @@ export default {
methods: { methods: {
monitorURL(id) { monitorURL(id) {
return "/dashboard/" + id; return "/dashboard/" + id;
} },
} },
} }
</script> </script>
@@ -138,10 +128,6 @@ export default {
} }
} }
.badge {
min-width: 58px;
}
.small-padding { .small-padding {
padding-left: 5px !important; padding-left: 5px !important;
padding-right: 5px !important; padding-right: 5px !important;

View File

@@ -1,15 +1,16 @@
<template> <template>
<div v-if="$route.name === 'DashboardHome'"> <div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">Quick Stats</h1> <h1 class="mb-3">
Quick Stats
</h1>
<div class="shadow-box big-padding text-center"> <div class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>Up</h3> <h3>Up</h3>
<span class="num">{{ stats.up }}</span> <span class="num">{{ stats.up }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Down</h3> <h3>Down</h3>
<span class="num text-danger">{{ stats.down }}</span> <span class="num text-danger">{{ stats.down }}</span>
</div> </div>
@@ -22,16 +23,16 @@
<span class="num text-secondary">{{ stats.pause }}</span> <span class="num text-secondary">{{ stats.pause }}</span>
</div> </div>
</div> </div>
<div class="row" v-if="false"> <div v-if="false" class="row">
<div class="col-3"> <div class="col-3">
<h3>Uptime</h3> <h3>Uptime</h3>
<p>(24-hour)</p> <p>(24-hour)</p>
<span class="num"></span> <span class="num" />
</div> </div>
<div class="col-3"> <div class="col-3">
<h3>Uptime</h3> <h3>Uptime</h3>
<p>(30-day)</p> <p>(30-day)</p>
<span class="num"></span> <span class="num" />
</div> </div>
</div> </div>
</div> </div>
@@ -39,26 +40,36 @@
<div class="shadow-box" style="margin-top: 25px;"> <div class="shadow-box" style="margin-top: 25px;">
<table class="table table-borderless table-hover"> <table class="table table-borderless table-hover">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Status</th> <th>Status</th>
<th>DateTime</th> <th>DateTime</th>
<th>Message</th> <th>Message</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="beat in importantHeartBeatList"> <tr v-for="(beat, index) in displayedRecords" :key="index">
<td>{{ beat.name }}</td> <td>{{ beat.name }}</td>
<td><Status :status="beat.status" /></td> <td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td> <td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td> <td>{{ beat.msg }}</td>
</tr> </tr>
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="4">No important events</td> <td colspan="4">
</tr> No important events
</td>
</tr>
</tbody> </tbody>
</table> </table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div> </div>
</div> </div>
@@ -68,8 +79,21 @@
<script> <script>
import Status from "../components/Status.vue"; import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue"; import Datetime from "../components/Datetime.vue";
import Pagination from "v-pagination-3";
export default { export default {
components: {Datetime, Status}, components: {
Datetime,
Status,
Pagination,
},
data() {
return {
page: 1,
perPage: 25,
heartBeatList: [],
}
},
computed: { computed: {
stats() { stats() {
let result = { let result = {
@@ -90,6 +114,8 @@ export default {
result.up++; result.up++;
} else if (beat.status === 0) { } else if (beat.status === 0) {
result.down++; result.down++;
} else if (beat.status === 2) {
result.up++;
} else { } else {
result.unknown++; result.unknown++;
} }
@@ -105,7 +131,7 @@ export default {
let result = []; let result = [];
for (let monitorID in this.$root.importantHeartbeatList) { for (let monitorID in this.$root.importantHeartbeatList) {
let list = this.$root.importantHeartbeatList[monitorID] let list = this.$root.importantHeartbeatList[monitorID]
result = result.concat(list); result = result.concat(list);
} }
@@ -120,16 +146,26 @@ export default {
result.sort((a, b) => { result.sort((a, b) => {
if (a.time > b.time) { if (a.time > b.time) {
return -1; return -1;
} else if (a.time < b.time) {
return 1;
} else {
return 0;
} }
if (a.time < b.time) {
return 1;
}
return 0;
}); });
this.heartBeatList = result;
return result; return result;
} },
}
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
} }
</script> </script>

View File

@@ -1,20 +1,28 @@
<template> <template>
<h1> {{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<p class="url"> <p class="url">
<a :href="monitor.url" target="_blank" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">{{ monitor.url }}</a> <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span> <span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
<br /> <br>
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span> <span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
</span> </span>
</p> </p>
<div class="functions"> <div class="functions">
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> <button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button> <font-awesome-icon icon="pause" /> Pause
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link> </button>
<button class="btn btn-danger" @click="deleteDialog">Delete</button> <button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<font-awesome-icon icon="play" /> Resume
</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
<font-awesome-icon icon="edit" /> Edit
</router-link>
<button class="btn btn-danger" @click="deleteDialog">
<font-awesome-icon icon="trash" /> Delete
</button>
</div> </div>
<div class="shadow-box"> <div class="shadow-box">
@@ -37,7 +45,7 @@
<span class="num"><CountUp :value="ping" /></span> <span class="num"><CountUp :value="ping" /></span>
</div> </div>
<div class="col"> <div class="col">
<h4>Avg.{{ pingTitle }}</h4> <h4>Avg. {{ pingTitle }}</h4>
<p>(24-hour)</p> <p>(24-hour)</p>
<span class="num"><CountUp :value="avgPing" /></span> <span class="num"><CountUp :value="avgPing" /></span>
</div> </div>
@@ -51,6 +59,56 @@
<p>(30-day)</p> <p>(30-day)</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span> <span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div> </div>
<div v-if="certInfo" class="col">
<h4>Cert Exp.</h4>
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
<span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
</span>
</div>
</div>
</div>
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
<div class="row">
<div class="col">
<h4>Certificate Info</h4>
<table class="text-start">
<tbody>
<tr class="my-3">
<td class="px-3">
Valid:
</td>
<td>{{ certInfo.valid }}</td>
</tr>
<tr class="my-3">
<td class="px-3">
Valid To:
</td>
<td><Datetime :value="certInfo.validTo" /></td>
</tr>
<tr class="my-3">
<td class="px-3">
Days Remaining:
</td>
<td>{{ certInfo.daysRemaining }}</td>
</tr>
<tr class="my-3">
<td class="px-3">
Issuer:
</td>
<td>{{ certInfo.issuer }}</td>
</tr>
<tr class="my-3">
<td class="px-3">
Fingerprint:
</td>
<td>{{ certInfo.fingerprint }}</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
@@ -64,30 +122,40 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="beat in importantHeartBeatList"> <tr v-for="(beat, index) in displayedRecords" :key="index">
<td><Status :status="beat.status" /></td> <td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td> <td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td> <td>{{ beat.msg }}</td>
</tr> </tr>
<tr v-if="importantHeartBeatList.length === 0"> <tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">No important events</td> <td colspan="3">
No important events
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div> </div>
<Confirm ref="confirmPause" @yes="pauseMonitor"> <Confirm ref="confirmPause" @yes="pauseMonitor">
Are you sure want to pause? Are you sure want to pause?
</Confirm> </Confirm>
<Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor"> <Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
Are you sure want to delete this monitor? Are you sure want to delete this monitor?
</Confirm> </Confirm>
</template> </template>
<script> <script>
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
import HeartbeatBar from "../components/HeartbeatBar.vue"; import HeartbeatBar from "../components/HeartbeatBar.vue";
@@ -95,6 +163,7 @@ import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue"; import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue"; import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3";
export default { export default {
components: { components: {
@@ -104,13 +173,14 @@ export default {
HeartbeatBar, HeartbeatBar,
Confirm, Confirm,
Status, Status,
}, Pagination,
mounted() {
}, },
data() { data() {
return { return {
page: 1,
perPage: 25,
heartBeatList: [],
toggleCertInfoBox: false,
} }
}, },
computed: { computed: {
@@ -118,9 +188,9 @@ export default {
pingTitle() { pingTitle() {
if (this.monitor.type === "http") { if (this.monitor.type === "http") {
return "Response" return "Response"
} else {
return "Ping"
} }
return "Ping"
}, },
monitor() { monitor() {
@@ -131,42 +201,65 @@ export default {
lastHeartBeat() { lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id] return this.$root.lastHeartbeatList[this.monitor.id]
} else { }
return { status: -1 }
return {
status: -1,
} }
}, },
ping() { ping() {
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) { if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
return this.lastHeartBeat.ping; return this.lastHeartBeat.ping;
} else {
return "N/A"
} }
return "N/A"
}, },
avgPing() { avgPing() {
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) { if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
return this.$root.avgPingList[this.monitor.id]; return this.$root.avgPingList[this.monitor.id];
} else {
return "N/A"
} }
return "N/A"
}, },
importantHeartBeatList() { importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) { if (this.$root.importantHeartbeatList[this.monitor.id]) {
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id] return this.$root.importantHeartbeatList[this.monitor.id]
} else {
return [];
} }
return [];
}, },
status() { status() {
if (this.$root.statusList[this.monitor.id]) { if (this.$root.statusList[this.monitor.id]) {
return this.$root.statusList[this.monitor.id] return this.$root.statusList[this.monitor.id]
} else {
return { }
} }
}
return { }
},
certInfo() {
if (this.$root.certInfoList[this.monitor.id]) {
return this.$root.certInfoList[this.monitor.id]
}
return null
},
showCertInfoBox() {
return this.certInfo != null && this.toggleCertInfoBox;
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
mounted() {
}, },
methods: { methods: {
@@ -204,9 +297,9 @@ export default {
toast.error(res.msg); toast.error(res.msg);
} }
}) })
} },
} },
} }
</script> </script>
@@ -251,4 +344,12 @@ table {
font-size: 13px; font-size: 13px;
color: #AAA; color: #AAA;
} }
.stats {
padding: 10px;
.col {
margin: 20px 0;
}
}
</style> </style>

View File

@@ -1,79 +1,121 @@
<template> <template>
<h1 class="mb-3">{{ pageName }}</h1> <h1 class="mb-3">
{{ pageName }}
</h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="shadow-box">
<div class="shadow-box"> <div class="row">
<div class="row"> <div class="col-md-6">
<div class="col-md-6"> <h2>General</h2>
<h2>General</h2>
<div class="mb-3"> <div class="mb-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">Monitor Type</label>
<select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http">HTTP(s)</option> <option value="http">
<option value="port">TCP Port</option> HTTP(s)
<option value="ping">Ping</option> </option>
<option value="keyword">HTTP(s) - Keyword</option> <option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - Keyword
</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" v-model="monitor.name" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'http' || monitor.type === 'keyword' "> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'keyword' "> <div v-if="monitor.type === 'keyword' " class="mb-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">Keyword</label>
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">Search keyword in plain html or JSON response and it is case-sensitive</div> <div class="form-text">
Search keyword in plain html or JSON response and it is case-sensitive
</div>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' "> <div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" v-model="monitor.hostname" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div> </div>
<div class="mb-3" v-if="monitor.type === 'port' "> <div v-if="monitor.type === 'port' " class="mb-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input type="number" class="form-control" id="port" v-model="monitor.port" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div>
<div class="mb-3">
<label for="maxRetries" class="form-label">Retries</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
Maximum retries before the service is marked as down and a notification is sent
</div>
</div>
<h2>Advanced</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites
</label>
</div>
<div class="mb-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down">
Upside Down Mode
</label>
<div class="form-text">
Flip the status upside down. If the service is reachable, it is DOWN.
</div>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button> <button class="btn btn-primary" type="submit" :disabled="processing">
Save
</button>
</div> </div>
</div>
<div class="col-md-6">
<div class="mt-3" v-if="$root.isMobile"></div>
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
<div class="form-check form-switch mb-3" v-for="notification in $root.notificationList">
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div> </div>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div> </div>
</div> </div>
</div>
</form> </form>
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
@@ -81,15 +123,12 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
},
mounted() {
this.init();
}, },
data() { data() {
return { return {
@@ -108,7 +147,15 @@ export default {
}, },
isEdit() { isEdit() {
return this.$route.path.startsWith("/edit"); return this.$route.path.startsWith("/edit");
} },
},
watch: {
"$route.fullPath" () {
this.init();
},
},
mounted() {
this.init();
}, },
methods: { methods: {
init() { init() {
@@ -119,7 +166,10 @@ export default {
name: "", name: "",
url: "https://", url: "https://",
interval: 60, interval: 60,
maxretries: 0,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false,
upsideDown: false,
} }
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@@ -154,12 +204,7 @@ export default {
this.$root.toastRes(res) this.$root.toastRes(res)
}) })
} }
} },
},
watch: {
'$route.fullPath' () {
this.init();
}
}, },
} }
</script> </script>

View File

@@ -1,94 +1,123 @@
<template> <template>
<h1 class="mb-3">Settings</h1> <h1 class="mb-3">
Settings
</h1>
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2>General</h2>
<form class="mb-3" @submit.prevent="saveGeneral"> <form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3"> <div class="mb-3">
<label for="timezone" class="form-label">Timezone</label> <label for="timezone" class="form-label">Timezone</label>
<select class="form-select" id="timezone" v-model="$root.userTimezone"> <select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">Auto: {{ guessTimezone }}</option> <option value="auto">
<option v-for="timezone in timezoneList" :value="timezone.value">{{ timezone.name }}</option> Auto: {{ guessTimezone }}
</option>
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
{{ timezone.name }}
</option>
</select> </select>
</div> </div>
<div> <div>
<button class="btn btn-primary" type="submit">Save</button> <button class="btn btn-primary" type="submit">
Save
</button>
</div> </div>
</form> </form>
<h2>Change Password</h2> <template v-if="loaded">
<form class="mb-3" @submit.prevent="savePassword"> <template v-if="! settings.disableAuth">
<div class="mb-3"> <h2>Change Password</h2>
<label for="current-password" class="form-label">Current Password</label> <form class="mb-3" @submit.prevent="savePassword">
<input type="password" class="form-control" id="current-password" required v-model="password.currentPassword"> <div class="mb-3">
</div> <label for="current-password" class="form-label">Current Password</label>
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
</div>
<div class="mb-3">
<label for="new-password" class="form-label">New Password</label>
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
</div>
<div class="mb-3">
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
<div class="invalid-feedback">
The repeat password does not match.
</div>
</div>
<div>
<button class="btn btn-primary" type="submit">
Update Password
</button>
</div>
</form>
</template>
<h2>Advanced</h2>
<div class="mb-3"> <div class="mb-3">
<label for="new-password" class="form-label">New Password</label> <button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword"> <button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
</div> </div>
</template>
<div class="mb-3">
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword">
<div class="invalid-feedback">
The repeat password is not match.
</div>
</div>
<div>
<button class="btn btn-primary" type="submit">Update Password</button>
</div>
</form>
<div>
<button class="btn btn-danger" @click="$root.logout">Logout</button>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<div class="mt-3" v-if="$root.isMobile"></div>
<h2>Notifications</h2> <h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p> <p v-if="$root.notificationList.length === 0">
<p v-else>Please assign the notification to monitor(s) to get it works.</p> Not available, please setup.
</p>
<p v-else>
Please assign a notification to monitor(s) to get it to work.
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;"> <ul class="list-group mb-3" style="border-radius: 1rem;">
<li class="list-group-item" v-for="notification in $root.notificationList"> <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br /> {{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a> <a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</li> </li>
</ul> </ul>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button> <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div> </div>
</div> </div>
</div> </div>
<NotificationDialog ref="notificationDialog" /> <NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</Confirm>
</template> </template>
<script> <script>
import Confirm from "../components/Confirm.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc' import utc from "dayjs/plugin/utc"
import timezone from 'dayjs/plugin/timezone' import timezone from "dayjs/plugin/timezone"
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
import {timezoneList} from "../util-frontend";
import { useToast } from 'vue-toastification' import { timezoneList } from "../util-frontend";
import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog NotificationDialog,
Confirm,
}, },
data() { data() {
return { return {
@@ -100,12 +129,21 @@ export default {
currentPassword: "", currentPassword: "",
newPassword: "", newPassword: "",
repeatNewPassword: "", repeatNewPassword: "",
} },
settings: {
},
loaded: false,
} }
}, },
watch: {
"password.repeatNewPassword"() {
this.invalidPassword = false;
},
},
mounted() { mounted() {
this.loadSettings();
}, },
methods: { methods: {
@@ -129,12 +167,37 @@ export default {
}) })
} }
}, },
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
this.loaded = true;
})
},
saveSettings() {
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
this.$root.toastRes(res);
this.loadSettings();
})
},
confirmDisableAuth() {
this.$refs.confirmDisableAuth.show();
},
disableAuth() {
this.settings.disableAuth = true;
this.saveSettings();
},
enableAuth() {
this.settings.disableAuth = false;
this.saveSettings();
this.$root.storage().removeItem("token");
},
}, },
watch: {
"password.repeatNewPassword"() {
this.invalidPassword = false;
}
}
} }
</script> </script>

View File

@@ -2,38 +2,42 @@
<div class="form-container"> <div class="form-container">
<div class="form"> <div class="form">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div> <div>
<object width="64" height="64" data="/icon.svg"></object> <object width="64" height="64" data="/icon.svg" />
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">Uptime Kuma</div> <div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
Uptime Kuma
</div>
</div> </div>
<p class="mt-3">Create your admin account</p> <p class="mt-3">
Create your admin account
</p>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username" required> <input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
<label for="floatingInput">Username</label> <label for="floatingInput">Username</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password" required> <input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
<label for="floatingPassword">Password</label> <label for="floatingPassword">Password</label>
</div> </div>
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="password" class="form-control" id="repeat" placeholder="Repeat Password" v-model="repeatPassword" required> <input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
<label for="repeat">Repeat Password</label> <label for="repeat">Repeat Password</label>
</div> </div>
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">Create</button> <button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
Create
</button>
</form> </form>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { useToast } from 'vue-toastification' import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
export default { export default {
@@ -70,8 +74,8 @@ export default {
this.$router.push("/") this.$router.push("/")
} }
}) })
} },
} },
} }
</script> </script>

View File

@@ -1,384 +1,372 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc' import timezone from "dayjs/plugin/timezone";
import timezone from 'dayjs/plugin/timezone' import utc from "dayjs/plugin/utc";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function ucfirst(str) {
if (! str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}
function getTimezoneOffset(timeZone) { function getTimezoneOffset(timeZone) {
const now = new Date(); const now = new Date();
const tzString = now.toLocaleString('en-US', { timeZone }); const tzString = now.toLocaleString("en-US", {
const localString = now.toLocaleString('en-US'); timeZone,
});
const localString = now.toLocaleString("en-US");
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000; const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
const offset = diff + now.getTimezoneOffset() / 60; const offset = diff + now.getTimezoneOffset() / 60;
return -offset; return -offset;
} }
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript // From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
// TODO: Move to separate file
const aryIannaTimeZones = [ const aryIannaTimeZones = [
'Europe/Andorra', "Europe/Andorra",
'Asia/Dubai', "Asia/Dubai",
'Asia/Kabul', "Asia/Kabul",
'Europe/Tirane', "Europe/Tirane",
'Asia/Yerevan', "Asia/Yerevan",
'Antarctica/Casey', "Antarctica/Casey",
'Antarctica/Davis', "Antarctica/Davis",
'Antarctica/Mawson', "Antarctica/Mawson",
'Antarctica/Palmer', "Antarctica/Palmer",
'Antarctica/Rothera', "Antarctica/Rothera",
'Antarctica/Syowa', "Antarctica/Syowa",
'Antarctica/Troll', "Antarctica/Troll",
'Antarctica/Vostok', "Antarctica/Vostok",
'America/Argentina/Buenos_Aires', "America/Argentina/Buenos_Aires",
'America/Argentina/Cordoba', "America/Argentina/Cordoba",
'America/Argentina/Salta', "America/Argentina/Salta",
'America/Argentina/Jujuy', "America/Argentina/Jujuy",
'America/Argentina/Tucuman', "America/Argentina/Tucuman",
'America/Argentina/Catamarca', "America/Argentina/Catamarca",
'America/Argentina/La_Rioja', "America/Argentina/La_Rioja",
'America/Argentina/San_Juan', "America/Argentina/San_Juan",
'America/Argentina/Mendoza', "America/Argentina/Mendoza",
'America/Argentina/San_Luis', "America/Argentina/San_Luis",
'America/Argentina/Rio_Gallegos', "America/Argentina/Rio_Gallegos",
'America/Argentina/Ushuaia', "America/Argentina/Ushuaia",
'Pacific/Pago_Pago', "Pacific/Pago_Pago",
'Europe/Vienna', "Europe/Vienna",
'Australia/Lord_Howe', "Australia/Lord_Howe",
'Antarctica/Macquarie', "Antarctica/Macquarie",
'Australia/Hobart', "Australia/Hobart",
'Australia/Currie', "Australia/Currie",
'Australia/Melbourne', "Australia/Melbourne",
'Australia/Sydney', "Australia/Sydney",
'Australia/Broken_Hill', "Australia/Broken_Hill",
'Australia/Brisbane', "Australia/Brisbane",
'Australia/Lindeman', "Australia/Lindeman",
'Australia/Adelaide', "Australia/Adelaide",
'Australia/Darwin', "Australia/Darwin",
'Australia/Perth', "Australia/Perth",
'Australia/Eucla', "Australia/Eucla",
'Asia/Baku', "Asia/Baku",
'America/Barbados', "America/Barbados",
'Asia/Dhaka', "Asia/Dhaka",
'Europe/Brussels', "Europe/Brussels",
'Europe/Sofia', "Europe/Sofia",
'Atlantic/Bermuda', "Atlantic/Bermuda",
'Asia/Brunei', "Asia/Brunei",
'America/La_Paz', "America/La_Paz",
'America/Noronha', "America/Noronha",
'America/Belem', "America/Belem",
'America/Fortaleza', "America/Fortaleza",
'America/Recife', "America/Recife",
'America/Araguaina', "America/Araguaina",
'America/Maceio', "America/Maceio",
'America/Bahia', "America/Bahia",
'America/Sao_Paulo', "America/Sao_Paulo",
'America/Campo_Grande', "America/Campo_Grande",
'America/Cuiaba', "America/Cuiaba",
'America/Santarem', "America/Santarem",
'America/Porto_Velho', "America/Porto_Velho",
'America/Boa_Vista', "America/Boa_Vista",
'America/Manaus', "America/Manaus",
'America/Eirunepe', "America/Eirunepe",
'America/Rio_Branco', "America/Rio_Branco",
'America/Nassau', "America/Nassau",
'Asia/Thimphu', "Asia/Thimphu",
'Europe/Minsk', "Europe/Minsk",
'America/Belize', "America/Belize",
'America/St_Johns', "America/St_Johns",
'America/Halifax', "America/Halifax",
'America/Glace_Bay', "America/Glace_Bay",
'America/Moncton', "America/Moncton",
'America/Goose_Bay', "America/Goose_Bay",
'America/Blanc-Sablon', "America/Blanc-Sablon",
'America/Toronto', "America/Toronto",
'America/Nipigon', "America/Nipigon",
'America/Thunder_Bay', "America/Thunder_Bay",
'America/Iqaluit', "America/Iqaluit",
'America/Pangnirtung', "America/Pangnirtung",
'America/Atikokan', "America/Atikokan",
'America/Winnipeg', "America/Winnipeg",
'America/Rainy_River', "America/Rainy_River",
'America/Resolute', "America/Resolute",
'America/Rankin_Inlet', "America/Rankin_Inlet",
'America/Regina', "America/Regina",
'America/Swift_Current', "America/Swift_Current",
'America/Edmonton', "America/Edmonton",
'America/Cambridge_Bay', "America/Cambridge_Bay",
'America/Yellowknife', "America/Yellowknife",
'America/Inuvik', "America/Inuvik",
'America/Creston', "America/Creston",
'America/Dawson_Creek', "America/Dawson_Creek",
'America/Fort_Nelson', "America/Fort_Nelson",
'America/Vancouver', "America/Vancouver",
'America/Whitehorse', "America/Whitehorse",
'America/Dawson', "America/Dawson",
'Indian/Cocos', "Indian/Cocos",
'Europe/Zurich', "Europe/Zurich",
'Africa/Abidjan', "Africa/Abidjan",
'Pacific/Rarotonga', "Pacific/Rarotonga",
'America/Santiago', "America/Santiago",
'America/Punta_Arenas', "America/Punta_Arenas",
'Pacific/Easter', "Pacific/Easter",
'Asia/Shanghai', "Asia/Shanghai",
'Asia/Urumqi', "Asia/Urumqi",
'America/Bogota', "America/Bogota",
'America/Costa_Rica', "America/Costa_Rica",
'America/Havana', "America/Havana",
'Atlantic/Cape_Verde', "Atlantic/Cape_Verde",
'America/Curacao', "America/Curacao",
'Indian/Christmas', "Indian/Christmas",
'Asia/Nicosia', "Asia/Nicosia",
'Asia/Famagusta', "Asia/Famagusta",
'Europe/Prague', "Europe/Prague",
'Europe/Berlin', "Europe/Berlin",
'Europe/Copenhagen', "Europe/Copenhagen",
'America/Santo_Domingo', "America/Santo_Domingo",
'Africa/Algiers', "Africa/Algiers",
'America/Guayaquil', "America/Guayaquil",
'Pacific/Galapagos', "Pacific/Galapagos",
'Europe/Tallinn', "Europe/Tallinn",
'Africa/Cairo', "Africa/Cairo",
'Africa/El_Aaiun', "Africa/El_Aaiun",
'Europe/Madrid', "Europe/Madrid",
'Africa/Ceuta', "Africa/Ceuta",
'Atlantic/Canary', "Atlantic/Canary",
'Europe/Helsinki', "Europe/Helsinki",
'Pacific/Fiji', "Pacific/Fiji",
'Atlantic/Stanley', "Atlantic/Stanley",
'Pacific/Chuuk', "Pacific/Chuuk",
'Pacific/Pohnpei', "Pacific/Pohnpei",
'Pacific/Kosrae', "Pacific/Kosrae",
'Atlantic/Faroe', "Atlantic/Faroe",
'Europe/Paris', "Europe/Paris",
'Europe/London', "Europe/London",
'Asia/Tbilisi', "Asia/Tbilisi",
'America/Cayenne', "America/Cayenne",
'Africa/Accra', "Africa/Accra",
'Europe/Gibraltar', "Europe/Gibraltar",
'America/Godthab', "America/Godthab",
'America/Danmarkshavn', "America/Danmarkshavn",
'America/Scoresbysund', "America/Scoresbysund",
'America/Thule', "America/Thule",
'Europe/Athens', "Europe/Athens",
'Atlantic/South_Georgia', "Atlantic/South_Georgia",
'America/Guatemala', "America/Guatemala",
'Pacific/Guam', "Pacific/Guam",
'Africa/Bissau', "Africa/Bissau",
'America/Guyana', "America/Guyana",
'Asia/Hong_Kong', "Asia/Hong_Kong",
'America/Tegucigalpa', "America/Tegucigalpa",
'America/Port-au-Prince', "America/Port-au-Prince",
'Europe/Budapest', "Europe/Budapest",
'Asia/Jakarta', "Asia/Jakarta",
'Asia/Pontianak', "Asia/Pontianak",
'Asia/Makassar', "Asia/Makassar",
'Asia/Jayapura', "Asia/Jayapura",
'Europe/Dublin', "Europe/Dublin",
'Asia/Jerusalem', "Asia/Jerusalem",
'Asia/Kolkata', "Asia/Kolkata",
'Indian/Chagos', "Indian/Chagos",
'Asia/Baghdad', "Asia/Baghdad",
'Asia/Tehran', "Asia/Tehran",
'Atlantic/Reykjavik', "Atlantic/Reykjavik",
'Europe/Rome', "Europe/Rome",
'America/Jamaica', "America/Jamaica",
'Asia/Amman', "Asia/Amman",
'Asia/Tokyo', "Asia/Tokyo",
'Africa/Nairobi', "Africa/Nairobi",
'Asia/Bishkek', "Asia/Bishkek",
'Pacific/Tarawa', "Pacific/Tarawa",
'Pacific/Enderbury', "Pacific/Enderbury",
'Pacific/Kiritimati', "Pacific/Kiritimati",
'Asia/Pyongyang', "Asia/Pyongyang",
'Asia/Seoul', "Asia/Seoul",
'Asia/Almaty', "Asia/Almaty",
'Asia/Qyzylorda', "Asia/Qyzylorda",
'Asia/Aqtobe', "Asia/Aqtobe",
'Asia/Aqtau', "Asia/Aqtau",
'Asia/Atyrau', "Asia/Atyrau",
'Asia/Oral', "Asia/Oral",
'Asia/Beirut', "Asia/Beirut",
'Asia/Colombo', "Asia/Colombo",
'Africa/Monrovia', "Africa/Monrovia",
'Europe/Vilnius', "Europe/Vilnius",
'Europe/Luxembourg', "Europe/Luxembourg",
'Europe/Riga', "Europe/Riga",
'Africa/Tripoli', "Africa/Tripoli",
'Africa/Casablanca', "Africa/Casablanca",
'Europe/Monaco', "Europe/Monaco",
'Europe/Chisinau', "Europe/Chisinau",
'Pacific/Majuro', "Pacific/Majuro",
'Pacific/Kwajalein', "Pacific/Kwajalein",
'Asia/Yangon', "Asia/Yangon",
'Asia/Ulaanbaatar', "Asia/Ulaanbaatar",
'Asia/Hovd', "Asia/Hovd",
'Asia/Choibalsan', "Asia/Choibalsan",
'Asia/Macau', "Asia/Macau",
'America/Martinique', "America/Martinique",
'Europe/Malta', "Europe/Malta",
'Indian/Mauritius', "Indian/Mauritius",
'Indian/Maldives', "Indian/Maldives",
'America/Mexico_City', "America/Mexico_City",
'America/Cancun', "America/Cancun",
'America/Merida', "America/Merida",
'America/Monterrey', "America/Monterrey",
'America/Matamoros', "America/Matamoros",
'America/Mazatlan', "America/Mazatlan",
'America/Chihuahua', "America/Chihuahua",
'America/Ojinaga', "America/Ojinaga",
'America/Hermosillo', "America/Hermosillo",
'America/Tijuana', "America/Tijuana",
'America/Bahia_Banderas', "America/Bahia_Banderas",
'Asia/Kuala_Lumpur', "Asia/Kuala_Lumpur",
'Asia/Kuching', "Asia/Kuching",
'Africa/Maputo', "Africa/Maputo",
'Africa/Windhoek', "Africa/Windhoek",
'Pacific/Noumea', "Pacific/Noumea",
'Pacific/Norfolk', "Pacific/Norfolk",
'Africa/Lagos', "Africa/Lagos",
'America/Managua', "America/Managua",
'Europe/Amsterdam', "Europe/Amsterdam",
'Europe/Oslo', "Europe/Oslo",
'Asia/Kathmandu', "Asia/Kathmandu",
'Pacific/Nauru', "Pacific/Nauru",
'Pacific/Niue', "Pacific/Niue",
'Pacific/Auckland', "Pacific/Auckland",
'Pacific/Chatham', "Pacific/Chatham",
'America/Panama', "America/Panama",
'America/Lima', "America/Lima",
'Pacific/Tahiti', "Pacific/Tahiti",
'Pacific/Marquesas', "Pacific/Marquesas",
'Pacific/Gambier', "Pacific/Gambier",
'Pacific/Port_Moresby', "Pacific/Port_Moresby",
'Pacific/Bougainville', "Pacific/Bougainville",
'Asia/Manila', "Asia/Manila",
'Asia/Karachi', "Asia/Karachi",
'Europe/Warsaw', "Europe/Warsaw",
'America/Miquelon', "America/Miquelon",
'Pacific/Pitcairn', "Pacific/Pitcairn",
'America/Puerto_Rico', "America/Puerto_Rico",
'Asia/Gaza', "Asia/Gaza",
'Asia/Hebron', "Asia/Hebron",
'Europe/Lisbon', "Europe/Lisbon",
'Atlantic/Madeira', "Atlantic/Madeira",
'Atlantic/Azores', "Atlantic/Azores",
'Pacific/Palau', "Pacific/Palau",
'America/Asuncion', "America/Asuncion",
'Asia/Qatar', "Asia/Qatar",
'Indian/Reunion', "Indian/Reunion",
'Europe/Bucharest', "Europe/Bucharest",
'Europe/Belgrade', "Europe/Belgrade",
'Europe/Kaliningrad', "Europe/Kaliningrad",
'Europe/Moscow', "Europe/Moscow",
'Europe/Simferopol', "Europe/Simferopol",
'Europe/Kirov', "Europe/Kirov",
'Europe/Astrakhan', "Europe/Astrakhan",
'Europe/Volgograd', "Europe/Volgograd",
'Europe/Saratov', "Europe/Saratov",
'Europe/Ulyanovsk', "Europe/Ulyanovsk",
'Europe/Samara', "Europe/Samara",
'Asia/Yekaterinburg', "Asia/Yekaterinburg",
'Asia/Omsk', "Asia/Omsk",
'Asia/Novosibirsk', "Asia/Novosibirsk",
'Asia/Barnaul', "Asia/Barnaul",
'Asia/Tomsk', "Asia/Tomsk",
'Asia/Novokuznetsk', "Asia/Novokuznetsk",
'Asia/Krasnoyarsk', "Asia/Krasnoyarsk",
'Asia/Irkutsk', "Asia/Irkutsk",
'Asia/Chita', "Asia/Chita",
'Asia/Yakutsk', "Asia/Yakutsk",
'Asia/Khandyga', "Asia/Khandyga",
'Asia/Vladivostok', "Asia/Vladivostok",
'Asia/Ust-Nera', "Asia/Ust-Nera",
'Asia/Magadan', "Asia/Magadan",
'Asia/Sakhalin', "Asia/Sakhalin",
'Asia/Srednekolymsk', "Asia/Srednekolymsk",
'Asia/Kamchatka', "Asia/Kamchatka",
'Asia/Anadyr', "Asia/Anadyr",
'Asia/Riyadh', "Asia/Riyadh",
'Pacific/Guadalcanal', "Pacific/Guadalcanal",
'Indian/Mahe', "Indian/Mahe",
'Africa/Khartoum', "Africa/Khartoum",
'Europe/Stockholm', "Europe/Stockholm",
'Asia/Singapore', "Asia/Singapore",
'America/Paramaribo', "America/Paramaribo",
'Africa/Juba', "Africa/Juba",
'Africa/Sao_Tome', "Africa/Sao_Tome",
'America/El_Salvador', "America/El_Salvador",
'Asia/Damascus', "Asia/Damascus",
'America/Grand_Turk', "America/Grand_Turk",
'Africa/Ndjamena', "Africa/Ndjamena",
'Indian/Kerguelen', "Indian/Kerguelen",
'Asia/Bangkok', "Asia/Bangkok",
'Asia/Dushanbe', "Asia/Dushanbe",
'Pacific/Fakaofo', "Pacific/Fakaofo",
'Asia/Dili', "Asia/Dili",
'Asia/Ashgabat', "Asia/Ashgabat",
'Africa/Tunis', "Africa/Tunis",
'Pacific/Tongatapu', "Pacific/Tongatapu",
'Europe/Istanbul', "Europe/Istanbul",
'America/Port_of_Spain', "America/Port_of_Spain",
'Pacific/Funafuti', "Pacific/Funafuti",
'Asia/Taipei', "Asia/Taipei",
'Europe/Kiev', "Europe/Kiev",
'Europe/Uzhgorod', "Europe/Uzhgorod",
'Europe/Zaporozhye', "Europe/Zaporozhye",
'Pacific/Wake', "Pacific/Wake",
'America/New_York', "America/New_York",
'America/Detroit', "America/Detroit",
'America/Kentucky/Louisville', "America/Kentucky/Louisville",
'America/Kentucky/Monticello', "America/Kentucky/Monticello",
'America/Indiana/Indianapolis', "America/Indiana/Indianapolis",
'America/Indiana/Vincennes', "America/Indiana/Vincennes",
'America/Indiana/Winamac', "America/Indiana/Winamac",
'America/Indiana/Marengo', "America/Indiana/Marengo",
'America/Indiana/Petersburg', "America/Indiana/Petersburg",
'America/Indiana/Vevay', "America/Indiana/Vevay",
'America/Chicago', "America/Chicago",
'America/Indiana/Tell_City', "America/Indiana/Tell_City",
'America/Indiana/Knox', "America/Indiana/Knox",
'America/Menominee', "America/Menominee",
'America/North_Dakota/Center', "America/North_Dakota/Center",
'America/North_Dakota/New_Salem', "America/North_Dakota/New_Salem",
'America/North_Dakota/Beulah', "America/North_Dakota/Beulah",
'America/Denver', "America/Denver",
'America/Boise', "America/Boise",
'America/Phoenix', "America/Phoenix",
'America/Los_Angeles', "America/Los_Angeles",
'America/Anchorage', "America/Anchorage",
'America/Juneau', "America/Juneau",
'America/Sitka', "America/Sitka",
'America/Metlakatla', "America/Metlakatla",
'America/Yakutat', "America/Yakutat",
'America/Nome', "America/Nome",
'America/Adak', "America/Adak",
'Pacific/Honolulu', "Pacific/Honolulu",
'America/Montevideo', "America/Montevideo",
'Asia/Samarkand', "Asia/Samarkand",
'Asia/Tashkent', "Asia/Tashkent",
'America/Caracas', "America/Caracas",
'Asia/Ho_Chi_Minh', "Asia/Ho_Chi_Minh",
'Pacific/Efate', "Pacific/Efate",
'Pacific/Wallis', "Pacific/Wallis",
'Pacific/Apia', "Pacific/Apia",
'Africa/Johannesburg', "Africa/Johannesburg",
]; ];
export function timezoneList() { export function timezoneList() {
let result = []; let result = [];
@@ -403,13 +391,14 @@ export function timezoneList() {
result.sort((a, b) => { result.sort((a, b) => {
if (a.time > b.time) { if (a.time > b.time) {
return 1; return 1;
} else if (b.time > a.time) {
return -1;
} else {
return 0;
} }
if (b.time > a.time) {
return -1;
}
return 0;
}) })
return result; return result;
}; }

34
src/util.js Normal file
View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0;
exports.DOWN = 0;
exports.UP = 1;
exports.PENDING = 2;
function flipStatus(s) {
if (s === exports.UP) {
return exports.DOWN;
}
if (s === exports.DOWN) {
return exports.UP;
}
return s;
}
exports.flipStatus = flipStatus;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
exports.sleep = sleep;
function ucfirst(str) {
if (!str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
function debug(msg) {
if (process.env.NODE_ENV === "development") {
console.log(msg);
}
}
exports.debug = debug;

43
src/util.ts Normal file
View File

@@ -0,0 +1,43 @@
// Common Util for frontend and backend
// Backend uses the compiled file util.js
// Frontend uses util.ts
// Need to run "tsc" to compile if there are any changes.
export const DOWN = 0;
export const UP = 1;
export const PENDING = 2;
export function flipStatus(s) {
if (s === UP) {
return DOWN;
}
if (s === DOWN) {
return UP;
}
return s;
}
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* PHP's ucfirst
* @param str
*/
export function ucfirst(str) {
if (! str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}
export function debug(msg) {
if (process.env.NODE_ENV === "development") {
console.log(msg)
}
}

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": false,
"files.insertFinalNewline": true
},
"files": [
"./server/util.ts"
]
}