Compare commits

...

497 Commits
1.0.1 ... 1.3.0

Author SHA1 Message Date
LouisLam
b1168d4cdb update to 1.3.0 2021-08-21 01:44:43 +08:00
LouisLam
f21937b197 add animation for page change 2021-08-20 02:37:59 +08:00
LouisLam
209e44c2e1 prevent all monitors making requests at the same moment when start the server 2021-08-19 18:41:31 +08:00
LouisLam
30b8d3d0ab prevent all monitors making requests at the same moment when start the server 2021-08-19 18:33:52 +08:00
LouisLam
64498163e1 add /list for mobile 2021-08-19 18:12:52 +08:00
LouisLam
4f70a70dda splite the left monitor list into a component 2021-08-19 18:05:14 +08:00
LouisLam
b761aaffdf Merge remote-tracking branch 'origin/master' 2021-08-19 17:49:27 +08:00
LouisLam
7ffdb2eb80 also backup sqlite shm, val file 2021-08-19 17:49:19 +08:00
Louis Lam
2d36f4cd4a Create SECURITY.md 2021-08-19 17:32:57 +08:00
Louis Lam
2339405f90 Update README.md 2021-08-19 14:13:30 +08:00
LouisLam
8f5e5ad944 install.sh - check docker is running 2021-08-19 12:47:11 +08:00
LouisLam
575c3ee182 install.sh - check docker is running 2021-08-19 12:39:51 +08:00
LouisLam
c9aa110f6c install.sh - check docker is running 2021-08-19 12:15:07 +08:00
LouisLam
bb0af35d47 wip: implementing install script 2021-08-19 02:38:45 +08:00
LouisLam
61944d642e wip: implementing install script 2021-08-19 02:04:49 +08:00
Louis Lam
de4515ea6e Merge pull request #230 from chakflying/patch-3
Fix: Improve chart styling on mobile
2021-08-18 23:21:18 +08:00
Nelson Chan
d8bcfcaaa2 Fix: Reduce chart padding on mobile 2021-08-18 17:04:13 +08:00
Nelson Chan
cf5168a4e6 Fix: Resize chart on screen breakpoints 2021-08-18 17:03:25 +08:00
LouisLam
f1e5e53e8f Merge branch 'patch-2' 2021-08-18 15:32:43 +08:00
LouisLam
432388a905 Merge branch 'Ponkhy_master' 2021-08-18 15:09:56 +08:00
LouisLam
746c1b6acc Merge remote-tracking branch 'origin/master' 2021-08-18 14:55:27 +08:00
LouisLam
269ac2410b install.sh add supports for CentOS 2021-08-18 14:55:03 +08:00
Nelson Chan
f72cdcc663 Feat: Add time to beat tooltip, misc. fixes 2021-08-18 11:40:20 +08:00
Louis Lam
6b3fbcd1e7 Merge pull request #228 from Ismaaa/patch-1
Fix typo in README.md
2021-08-18 01:06:36 +08:00
Ismail D
8a48f5dd71 Fix typo in README.md 2021-08-17 17:59:34 +02:00
LouisLam
d218661f3d wip: implementing install script 2021-08-17 23:08:35 +08:00
Ponkhy
77369bd002 Fixed invisible heartbeat bar after page switch 2021-08-17 15:40:22 +02:00
Louis Lam
29a89df524 Merge pull request #227 from Ponkhy/line-messenger
Added Line Messenger Notification Service
2021-08-17 20:25:01 +08:00
Louis Lam
e257fa7b2d Merge pull request #221 from chakflying/patch-1
Fix: Improve Chart axis, use 24Hr format
2021-08-17 20:05:48 +08:00
LouisLam
6980c38a6c fix #226 a workaround fix similar to https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 2021-08-17 20:00:31 +08:00
LouisLam
8d57df7256 fix #226 a workaround fix similar to https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 2021-08-17 19:58:09 +08:00
Ponkhy
64501bf065 Added Line Messenger Notification Service 2021-08-17 13:41:36 +02:00
LouisLam
440c178403 change sqlite to WAL mode 2021-08-17 18:18:41 +08:00
LouisLam
c9c51e47e1 add some comments 2021-08-17 16:43:59 +08:00
LouisLam
5e52f230b1 create datetime mixin 2021-08-17 16:41:12 +08:00
LouisLam
61e758d872 disable pool for sqlite, re-use a connection to improve the performance. 2021-08-17 15:59:23 +08:00
LouisLam
86826fb826 Merge remote-tracking branch 'origin/master' 2021-08-17 15:32:55 +08:00
LouisLam
7a32e5e6ff catch rejection error globally 2021-08-17 15:32:34 +08:00
Louis Lam
610f2f9c47 Merge pull request #225 from AverageHumanoid/master
Add ping support in FreeBSD
2021-08-17 14:03:28 +08:00
AverageHumanoid
01e9c76a6f Use ping in FreeBSD 2021-08-16 19:48:37 -07:00
Ponkhy
5927c2703f Merge branch 'louislam:master' into master 2021-08-17 02:55:41 +02:00
LouisLam
316db89b9a Merge remote-tracking branch 'origin/master' 2021-08-17 02:09:56 +08:00
LouisLam
eed6d3e847 add more query log for dev env 2021-08-17 02:09:40 +08:00
Louis Lam
2a62f6daae Update ask-for-help.md 2021-08-17 01:32:42 +08:00
Louis Lam
e09c296410 Update bug_report.md 2021-08-17 01:32:21 +08:00
Louis Lam
d7f660ec57 Update bug_report.md 2021-08-17 01:29:34 +08:00
LouisLam
798f39acf0 Merge remote-tracking branch 'origin/master' 2021-08-17 01:26:35 +08:00
LouisLam
31d5b4fd3d do not pass smtp user/pass to nodemailer if both are empty 2021-08-17 01:26:21 +08:00
LouisLam
fc76c2836b increase the query timeout 2021-08-17 01:22:22 +08:00
Nelson Chan
0b30bfff87 Fix: Improve Chart axis, use 24Hr format 2021-08-16 23:58:02 +08:00
Ponkhy
72f0724b9a Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:21 +02:00
Ponkhy
35176a614f Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:13 +02:00
Ponkhy
8e883c9c6a Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:05 +02:00
Louis Lam
2f89ee4937 Update README.md 2021-08-16 21:31:05 +08:00
LouisLam
5d0b6190c3 update to 1.2.0 2021-08-16 20:40:28 +08:00
LouisLam
cb85905c33 minor 2021-08-16 20:40:16 +08:00
Ponkhy
233c5661af Added user choice heartbeat bar 2021-08-15 20:46:21 +02:00
Ponkhy
91d4c15b4d Merge branch 'louislam:master' into master 2021-08-15 20:28:30 +02:00
LouisLam
981ed5f29f Merge remote-tracking branch 'origin/master' 2021-08-16 01:42:54 +08:00
LouisLam
0b45694f2f update all dependencies 2021-08-16 01:42:39 +08:00
Louis Lam
60531d0b15 Update README.md 2021-08-16 01:38:37 +08:00
Louis Lam
a3de63ac3c Update README.md 2021-08-16 01:29:47 +08:00
Louis Lam
80eadcb236 Merge pull request #214 from ChrisTheBaron/pushbullet
Add Pushbullet notification service
2021-08-16 01:06:27 +08:00
LouisLam
7e5a8c896b Merge remote-tracking branch 'chakflying/ping-graph' 2021-08-16 01:00:18 +08:00
Chris Taylor
efe75bde75 Add Pushbullet notification service 2021-08-13 21:18:43 +01:00
Louis Lam
af34e861c5 Merge pull request #200 from proffalken/feature/187_add_cert_checks_to_prometheus
Add certificate monitoring to the Prometheus handler
2021-08-13 00:26:58 +08:00
Louis Lam
2ae2022e62 Merge pull request #211 from AlexandreGagner/master
Add Octopush Notification Service
2021-08-13 00:26:35 +08:00
LouisLam
37f1d60f82 also change meta tag theme-color 2021-08-13 00:23:40 +08:00
LouisLam
d39b43dacc fix require problem 2021-08-13 00:13:46 +08:00
Louis Lam
7ca80fc086 fix auto theme 2021-08-12 22:17:20 +08:00
Alexandre Gagner
eb34dc6cc2 Update notification.js
Fix remove non ascii char from msg
2021-08-12 00:58:51 +02:00
Alexandre Gagner
ed93aae1c2 add octopush notification service 2021-08-12 00:15:53 +02:00
Ponkhy
e1a38f64f8 Merge branch 'louislam:master' into master 2021-08-11 21:19:33 +02:00
LouisLam
6a8ccf627a add version to user agent 2021-08-12 01:31:07 +08:00
Nelson Chan
8f150aaeb9 Feat: Use Async Component 2021-08-12 00:47:58 +08:00
Nelson Chan
6ed1d8cb2f Feat: Use selective import, improve tooltip UI 2021-08-12 00:31:21 +08:00
Nelson Chan
71bec74081 Feat: Add down-ed bars, improve UI 2021-08-11 23:40:56 +08:00
Nelson Chan
2bd735035c Misc: Show graph by default 2021-08-11 23:40:56 +08:00
Nelson Chan
48c6d8f19f Feat: Display recent ping chart 2021-08-11 23:40:51 +08:00
LouisLam
2d176a38af Merge remote-tracking branch 'origin/master' 2021-08-11 23:38:48 +08:00
LouisLam
b14f63491d timeout change to 80% of its interval 2021-08-11 23:12:38 +08:00
LouisLam
24b87fcd5a update vue to 3.2.1 2021-08-11 22:41:33 +08:00
Ponkhy
45c162583b Added more space over the badge on mobile screens 2021-08-11 11:16:53 +02:00
LouisLam
365ea0a189 add batsh 2021-08-11 14:52:25 +08:00
Louis Lam
2461f5084e Merge pull request #205 from Ponkhy/master
Fixed function buttons for smaller screens
2021-08-11 00:50:43 +08:00
Ponkhy
1d0b332b42 Fixed function buttons for smaller screens 2021-08-10 18:29:47 +02:00
LouisLam
d5149f90b4 fix ping 2021-08-10 22:00:29 +08:00
LouisLam
e0ae9a9e73 improve space-before-function-paren 2021-08-10 21:59:15 +08:00
LouisLam
3227a2660b log undefined ping 2021-08-10 21:47:14 +08:00
LouisLam
764160f38c add eslint: space-before-function-paren 2021-08-10 21:44:29 +08:00
LouisLam
70e7945a66 fix possible race condition 2021-08-10 21:37:51 +08:00
LouisLam
b413427a37 graceful shutdown when listen error 2021-08-10 21:28:54 +08:00
LouisLam
debcac4924 run eslint 2021-08-10 14:24:05 +01:00
Matthew Macdonald-Wallace
268dd33792 Add TLS Info to Prometheus metric output 2021-08-10 14:24:05 +01:00
LouisLam
692a11e51e pass tls info to prometheus.update 2021-08-10 14:24:05 +01:00
Matthew Macdonald-Wallace
5eb4f55dfd Add the new gauges to the prometheus handler 2021-08-10 14:24:05 +01:00
LouisLam
e7cc5340e5 ping ipv6 for macos 2021-08-10 21:07:11 +08:00
LouisLam
4d4d504d6e retry ping domain with ipv6, if domain is not found 2021-08-10 21:03:14 +08:00
LouisLam
2a4695a774 add -6 to ping cmd if ipv6 address 2021-08-10 20:39:58 +08:00
LouisLam
f089bf73c3 Merge remote-tracking branch 'origin/master' 2021-08-10 20:23:29 +08:00
LouisLam
f099e4270d change to Accept: */* to better support all websites 2021-08-10 20:23:15 +08:00
Louis Lam
81636c7b44 Delete reviewdog.yml 2021-08-10 20:03:25 +08:00
Louis Lam
98fa995d3f Update reviewdog.yml 2021-08-10 19:19:55 +08:00
Louis Lam
42d24258cf Delete app.json 2021-08-10 18:41:38 +08:00
Louis Lam
3f56167198 Update reviewdog.yml 2021-08-10 17:56:30 +08:00
Louis Lam
5163e16482 Update reviewdog.yml 2021-08-10 17:36:45 +08:00
LouisLam
d93f6e2716 server.listen bind to ipv6 too 2021-08-10 16:45:37 +08:00
LouisLam
d6fad7f1ef server.listen bind to ipv6 too 2021-08-10 16:36:21 +08:00
LouisLam
5512b15162 add better token for github-pr-review for reviewdog 2021-08-10 15:39:39 +08:00
LouisLam
8979311653 Merge remote-tracking branch 'origin/master' 2021-08-10 15:04:15 +08:00
LouisLam
4f058c5b47 do not fix height for h1 2021-08-10 15:04:01 +08:00
LouisLam
9ba1743900 split mobile mixin from socket mixin 2021-08-10 15:02:46 +08:00
Louis Lam
1e4f9c7e15 Update README.md 2021-08-10 13:10:03 +08:00
Louis Lam
974672f7c1 Delete deploy.template.yaml 2021-08-10 13:09:36 +08:00
Louis Lam
01ac6d54be Merge pull request #199 from chakflying/patch-1
Fix: unify styling of theme switch btn
2021-08-10 12:50:50 +08:00
Nelson Chan
113899e278 Fix: unify styling of theme switch with UI 2021-08-10 12:20:06 +08:00
LouisLam
d1d000bd74 remove red circle around the btn-close while focus 2021-08-10 00:16:13 +08:00
Louis Lam
ef4677a640 Merge pull request #194 from Ponkhy/master
Fixed Close Button Color in Dark Mode
2021-08-10 00:04:25 +08:00
Ponkhy
e39c46ff9b Fixed Close Button Color in Dark Mode 2021-08-09 17:56:44 +02:00
Louis Lam
0e46ce42d1 Update README.md 2021-08-09 22:31:32 +08:00
LouisLam
efc9a254f4 update to 1.1.0 2021-08-09 21:03:30 +08:00
LouisLam
116d803592 minor 2021-08-09 21:03:22 +08:00
LouisLam
ba1d271afa fix jwt error 2021-08-09 20:09:01 +08:00
LouisLam
12910b23ed cache more layers for docker build 2021-08-09 19:23:18 +08:00
LouisLam
550c9703a6 fix radio button not checked 2021-08-09 18:46:57 +08:00
LouisLam
b69185ee9e control search engine visibility 2021-08-09 18:16:27 +08:00
LouisLam
ddcfa558f7 Merge remote-tracking branch 'origin/master' 2021-08-09 15:45:59 +08:00
LouisLam
478d2c4e8c add more favicon 2021-08-09 15:44:32 +08:00
Louis Lam
1352a0a162 Update bug_report.md 2021-08-09 14:53:21 +08:00
Louis Lam
7274b82143 Update ask-for-help.md 2021-08-09 14:52:55 +08:00
Louis Lam
69b1454cf5 Update feature_request.md 2021-08-09 14:52:31 +08:00
LouisLam
8f2a9fe883 chnage sqlite3 package in dockerfile 2021-08-09 14:47:53 +08:00
Louis Lam
1b8476417d Update README.md 2021-08-09 14:47:10 +08:00
Louis Lam
2a65402ad8 fix update command 2021-08-09 14:11:26 +08:00
LouisLam
59ef1f13db set longer timeout for axios request 2021-08-09 13:54:24 +08:00
LouisLam
bf33f97c9e code re-use and eslint 2021-08-09 13:49:37 +08:00
LouisLam
d0aad3400c add reset password in cli 2021-08-09 13:34:44 +08:00
LouisLam
6f489e7e0f Accepted Status Codes / Max Redirects for http/keyword only 2021-08-09 02:01:08 +08:00
LouisLam
f9cb8293f3 improve a bit ux 2021-08-09 01:58:56 +08:00
LouisLam
11b8c61079 wip: add search engine control in setting 2021-08-09 01:58:24 +08:00
Louis Lam
f69ba12c10 update reviewdog, add vue,ts ext 2021-08-09 00:33:28 +08:00
Louis Lam
e78cfaa492 Merge pull request #189 from Saibamen/save_maxredirects
Save `maxredirects` on monitor edit
2021-08-09 00:27:44 +08:00
Adam Stachowicz
9c17f59fe8 Fix few markdown lint warnings 2021-08-08 18:24:30 +02:00
Adam Stachowicz
519add4fab ESLint vite.config.js 2021-08-08 18:24:05 +02:00
Adam Stachowicz
46c7e5d058 Save maxredirects on edit 2021-08-08 18:23:51 +02:00
LouisLam
6291b7b8bb update reviewdog 2021-08-09 00:12:35 +08:00
LouisLam
3fb515e871 Merge remote-tracking branch 'origin/master' 2021-08-09 00:09:45 +08:00
LouisLam
8e440f7dff add a bot for eslint on github 2021-08-09 00:09:33 +08:00
Louis Lam
6d58c98b24 Update README.md 2021-08-08 23:51:23 +08:00
LouisLam
6ca7ca4e7e improve alignment and font size 2021-08-08 21:42:37 +08:00
Louis Lam
44391117ab Merge pull request #173 from chakflying/redirects&status
Feat: Implement Max.Redirects & Accepted Status Codes
2021-08-08 21:19:20 +08:00
LouisLam
9fa8d5c1fa improve multiselect 2021-08-08 21:14:29 +08:00
LouisLam
3265c3cbc3 improve multiselect 2021-08-08 21:03:10 +08:00
Louis Lam
b3721e03a8 Merge pull request #186 from chakflying/patch-3
Chore: Improve logging during db development
2021-08-08 19:24:50 +08:00
Nelson Chan
4ff68238c4 Chore: Improve logging during db development 2021-08-08 15:04:20 +08:00
LouisLam
7b1000d995 Merge remote-tracking branch 'chakflying/redirects&status' into redirects&status 2021-08-08 15:03:39 +08:00
LouisLam
a79e6aa338 dark theme for multiselect 2021-08-08 15:02:33 +08:00
LouisLam
3005585c0f Merge branch 'master' into redirects&status 2021-08-08 14:48:00 +08:00
Philipp Dormann
123fca43a1 FEAT: darkmode (#155)
* darkmode fixes

* fix: darkmode: empty beats in active/ hovered state

* fix: color for empty beats

* fix: navbar background color

* Update src/assets/vars.scss

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>

* Update src/assets/app.scss

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>

* wip, split dark theme style by .dark and store light theme to normal

* add back missing css

* working switch theme button and tuning dark theme

* finish dark theme

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Co-authored-by: LouisLam <louislam@users.noreply.github.com>
2021-08-08 13:47:29 +08:00
LouisLam
d5b40dfebf better code reuse and "Username" to "Bot Display Name" 2021-08-08 11:03:22 +08:00
LouisLam
c990edc87d allowElseIf for else return, since its auto fix removes "else" but without newline 2021-08-08 02:34:51 +08:00
LouisLam
2677f5dd87 run eslint for discord enhancement 2021-08-08 02:18:33 +08:00
Niyas
4469b3a19b Added discord username field 2021-08-07 11:13:25 +05:30
Niyas
ebf207c2f5 Custom embed username 2021-08-07 11:12:36 +05:30
Nelson Chan
a50aa93e84 Fix: Fix monitor creation json parsing 2021-08-07 02:10:38 +08:00
Niyas
91fce75a93 Removed UptimeKuma url field 2021-08-06 17:38:16 +05:30
Niyas
3a7414125a Updated discord embeds 2021-08-06 17:37:22 +05:30
LouisLam
5a6e5b7948 change multiselect color 2021-08-06 19:48:51 +08:00
LouisLam
adcd251076 Merge branch 'master' into redirects&status 2021-08-06 19:26:44 +08:00
LouisLam
dadc270876 Merge branch 'master' into discord-enhancements 2021-08-06 19:13:43 +08:00
LouisLam
a98ba41c8e minor 2021-08-06 19:12:49 +08:00
LouisLam
a40816b948 fix high severity vulnerabilities by using my fork sqlite3 package 2021-08-06 19:09:00 +08:00
LouisLam
d3e24df225 fix high severity vulnerabilities by using my fork sqlite3 package 2021-08-06 18:22:30 +08:00
Niyas
908176c910 Discord enhancements 2021-08-05 21:42:45 +05:30
Niyas
9ade9af1e2 Discord enhancements 2021-08-05 21:41:11 +05:30
LouisLam
8350bff629 update dependencies 2021-08-05 22:46:48 +08:00
Nelson Chan
93ea2c277a Update src/pages/EditMonitor.vue
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-05 21:06:47 +08:00
LouisLam
6251f47050 fix the min height of monitor list 2021-08-05 19:56:20 +08:00
Nelson Chan
8f7885e58a Feat: Implement MaxRedirects & StatusCodes 2021-08-05 19:04:38 +08:00
LouisLam
dffe3cf8f2 Revert "try to support subdirectory reverse proxy"
This reverts commit a03dd91e40.
2021-08-05 18:20:34 +08:00
LouisLam
d411143f3c Merge remote-tracking branch 'origin/master' 2021-08-05 17:56:50 +08:00
LouisLam
a03dd91e40 try to support subdirectory reverse proxy 2021-08-05 17:56:38 +08:00
Louis Lam
2c2ac9dc59 Delete dependabot.yml 2021-08-05 12:13:39 +08:00
Louis Lam
d06711a1a7 Update README.md 2021-08-04 15:19:06 +08:00
Louis Lam
94f2219715 Create dependabot.yml 2021-08-04 14:51:05 +08:00
LouisLam
d315e8306b update to 1.0.10 2021-08-04 13:59:42 +08:00
LouisLam
8cd0e7a058 a better script for version update 2021-08-04 13:53:21 +08:00
LouisLam
8fce62632d a better script for version update 2021-08-04 13:53:13 +08:00
LouisLam
38c0c170e7 add some comments 2021-08-04 13:31:17 +08:00
Nelson Chan
655536e457 Fix: use send() instead of end() (#161) 2021-08-04 11:56:10 +08:00
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
LouisLam
657acf748b update to 1.0.6 2021-07-20 20:04:54 +08:00
Sam
054269ecf0 fix notification when changing from pending -> up 2021-07-20 11:50:33 +02:00
Louis Lam
71dd68bb6d Update README.md 2021-07-20 13:04:21 +08: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
Louis Lam
58d029445d Merge pull request #79 from Saibamen/remove_debug_spam
Remove debug spam
2021-07-19 23:12:33 +08:00
LouisLam
77af41bfff env default to production 2021-07-19 23:06:42 +08:00
Louis Lam
058032a26a Merge pull request #81 from Saibamen/use_NODE_ENV
Use `NODE_ENV` from Express and Socket.IO
2021-07-19 22:59:36 +08:00
Louis Lam
cbb9d3f91b set version for docker 2021-07-19 20:18:27 +08:00
Adam Stachowicz
5bd3184ebf Use connect_error event 2021-07-18 20:59:00 +02:00
Adam Stachowicz
59ebe134f1 Fix indentation 2021-07-18 20:46:45 +02:00
Adam Stachowicz
851ceef3d5 Use NODE_ENV from Express and Socket.IO 2021-07-18 20:21:17 +02:00
LouisLam
efd7608ba2 Merge remote-tracking branch 'origin/master' 2021-07-19 00:43:45 +08:00
LouisLam
05fdaf0c96 update package-lock.json 2021-07-19 00:43:25 +08:00
Louis Lam
01b0e82d52 Update README.md 2021-07-19 00:40:10 +08:00
Louis Lam
d2ccfd5366 Update README.md 2021-07-19 00:39:07 +08:00
Adam Stachowicz
7cba9ce231 Remove debug spam 2021-07-18 18:35:40 +02:00
Louis Lam
69e8c56e3e Update README.md 2021-07-19 00:34:48 +08:00
Adam Stachowicz
9928ea8c30 Merge branch 'master' into simple_pagination 2021-07-18 16:48:53 +02:00
LouisLam
25c370c9ff Merge branch 'update_packages'
# Conflicts:
#	package-lock.json
#	package.json
2021-07-18 22:22:19 +08:00
LouisLam
9227ff6ea3 add nightly build for amd64 only 2021-07-18 22:21:34 +08:00
Adam Stachowicz
2d943620c7 Merge branch 'master' into simple_pagination 2021-07-18 15:37:32 +02:00
Louis Lam
f7bd67c413 Merge pull request #77 from Saibamen/fix_docker
Fix Docker build
2021-07-18 20:53:06 +08:00
LouisLam
9ca2444dab improve testing notification response 2021-07-18 20:49:46 +08:00
Adam Stachowicz
1d45a7606d Fix Docker build 2021-07-18 13:51:44 +02:00
Adam Stachowicz
16f363ac38 Merge branch 'master' into simple_pagination 2021-07-18 13:36:57 +02:00
Louis Lam
d6b9403f60 Merge pull request #76 from Saibamen/fix_remember_me
Fix multiple labels for `Remember me`
2021-07-18 19:28:48 +08:00
Louis Lam
92e5ddd97c Merge pull request #69 from Saibamen/dockerignore
Update .dockerignore
2021-07-18 19:27:43 +08:00
Louis Lam
23611e540c Merge pull request #70 from Saibamen/fix_npm_warnings
Fix NPM warnings
2021-07-18 19:27:00 +08:00
Adam Stachowicz
f9274557f3 Fix center 2021-07-18 13:22:39 +02:00
Adam Stachowicz
44b66cbd2e Fix Remember me label 2021-07-18 13:00:59 +02:00
LouisLam
66037e236c add apprise support 2021-07-18 18:51:58 +08:00
Adam Stachowicz
386c002fda Merge branch 'master' into fix_npm_warnings 2021-07-18 11:48:08 +02:00
Adam Stachowicz
7c1aab6a15 Merge branch 'master' into dockerignore 2021-07-18 11:47:53 +02:00
Adam Stachowicz
37884cfd08 Merge branch 'master' into update_packages 2021-07-18 11:47:08 +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
LouisLam
13c9244e3f fix apprise import issue and loose the healthcheck rule 2021-07-18 17:42:00 +08:00
Louis Lam
13b3a5be9c Merge pull request #68 from Saibamen/use_console_error
Improve printing to console
2021-07-18 14:55:30 +08:00
Louis Lam
2e31e780a1 Merge pull request #67 from Saibamen/lighthouse_improvements
[Lighthouse] Some improvements
2021-07-18 13:48:35 +08:00
Louis Lam
6a2b5f9dd8 Merge pull request #66 from Saibamen/resize_apple_icon
[Lighthouse] Resize apple icon to 192px
2021-07-18 13:47:50 +08:00
Louis Lam
16767dc042 Merge pull request #65 from Saibamen/robots
[Lighthouse] Add robots.txt
2021-07-18 13:47:39 +08:00
Louis Lam
fb3e000dc3 Merge pull request #63 from NiNiyas/docker-healthcheck
Docker Healthcheck
2021-07-18 13:47:25 +08:00
Louis Lam
6f3ea21864 Merge pull request #61 from NiNiyas/slack-enhancements
Slack Enhancements and aligns footer to center
2021-07-18 13:47:04 +08:00
Louis Lam
403137280e Merge pull request #62 from NiNiyas/pushover-support
Pushover support
2021-07-18 12:20:41 +08: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
Adam Stachowicz
9c248776e7 Update dependencies 2021-07-18 00:08:35 +02:00
Adam Stachowicz
309caa4279 Fix indentation 2021-07-17 23:57:48 +02:00
Adam Stachowicz
28b14ceb70 Fix NPM warnings 2021-07-17 23:45:36 +02:00
Adam Stachowicz
9f9c42c30b Update .dockerignore 2021-07-17 23:31:44 +02:00
Adam Stachowicz
2bff62cade Improve printing to console 2021-07-17 23:13:54 +02:00
Adam Stachowicz
bfb4a5bcd4 Add alt="Logo" 2021-07-17 22:56:54 +02:00
Adam Stachowicz
e87b78501b rel="noopener" for external link 2021-07-17 22:55:53 +02:00
Adam Stachowicz
637422494f Add description 2021-07-17 22:53:32 +02:00
Adam Stachowicz
db34484ff2 Add theme-color 2021-07-17 22:51:07 +02:00
Adam Stachowicz
790c071f2c Resize apple icon to 192px 2021-07-17 22:45:28 +02:00
Adam Stachowicz
149688e669 Add robots.txt 2021-07-17 22:24:10 +02:00
Niyas
7dae5279fb Docker healthcheck
Copied from https://scoutapm.com/blog/how-to-use-docker-healthcheck
2021-07-17 20:08:01 +05:30
Niyas
c203317b3b Docker healthcheck
Copied from https://scoutapm.com/blog/how-to-use-docker-healthcheck
2021-07-17 20:07:35 +05:30
Niyas
01f2fccb23 Update notification.js 2021-07-17 18:59:26 +05:30
Niyas
7808aef58f Pushover support 2021-07-17 18:42:54 +05:30
Niyas
ce2d78f45a Pushover support 2021-07-17 17:55:02 +05:30
Niyas
20cad50593 Pushover support 2021-07-17 17:49:56 +05:30
Niyas
829a2a191d Footer center align 2021-07-17 13:42:05 +05:30
Niyas
65b320d06b Slack Enhancements 2021-07-17 12:48:42 +05:30
Niyas
1935da5b16 Slack Enhancements 2021-07-17 12:47:52 +05:30
LouisLam
78f5d2cd8b update to 1.0.5 2021-07-17 02:31:31 +08:00
LouisLam
f62b70c9a9 add nightly to version number 2021-07-17 02:30:16 +08:00
LouisLam
dfa9b3a0ca fix require() actually not working after build in the frontend 2021-07-17 00:51:28 +08:00
LouisLam
b3bff8d735 add graceful shutdown 2021-07-16 01:44:51 +08:00
Louis Lam
f2af5bc064 Merge pull request #46 from NiNiyas/slack-webhook
Added Slack webhook notification
2021-07-15 11:59:41 +08:00
Louis Lam
91b736f391 Merge pull request #52 from philippdormann/feature/gotify-upstream-merge
customize Gotify priority
2021-07-15 11:59:20 +08:00
Louis Lam
275f77d4bb Merge pull request #45 from R0GGER/master
Apple icon for iPhone/iPad
2021-07-15 11:03:46 +08:00
Louis Lam
b00524067a Update README.md 2021-07-15 10:59:36 +08:00
Philipp Dormann
25a93b05dc easier merging 🤞 2021-07-14 22:00:15 +02:00
Philipp Dormann
53e203d2f9 add gotify priority
ref https://github.com/louislam/uptime-kuma/pull/43
closes https://github.com/louislam/uptime-kuma/issues/50
2021-07-14 21:56:38 +02:00
LouisLam
f48f957ba9 update to 1.0.4 2021-07-15 01:44:15 +08:00
LouisLam
bfb117cb76 minor 2021-07-15 01:01:47 +08:00
LouisLam
2b8e33caed dockerfile: change the base image to node:14-alpine3.12; add apprise cli, prepare for implementing notification 2021-07-15 00:36:44 +08:00
Niyas
60493f0f86 Updated Slack test notification 2021-07-14 21:59:16 +05:30
Niyas
63c6e29e62 Added Slack Webhook support 2021-07-14 21:08:38 +05:30
Niyas
5f6d5588a6 Added Slack Webhook support 2021-07-14 21:07:14 +05:30
R0GGER
18744d834f Add files via upload 2021-07-14 14:35:59 +02:00
R0GGER
8dd5b97b79 Apple icon 2021-07-14 14:34:50 +02:00
Louis Lam
386c8bfdf1 Merge pull request #43 from philippdormann/feature/gotify-upstream-merge
 Gotify Support
2021-07-14 17:36:21 +08:00
Philipp Dormann
126f00e739 added Gotify Support 2021-07-14 11:25:10 +02:00
Louis Lam
80466ac957 Update README.md 2021-07-14 17:10:51 +08:00
LouisLam
3b52433202 cache the sqlite built when docker build 2021-07-14 12:42:52 +08:00
Louis Lam
137f5da3da Update README.md 2021-07-14 01:48:55 +08:00
Louis Lam
338d002d42 Update README.md 2021-07-14 01:39:04 +08:00
Louis Lam
77ab9fbc57 Add some shields by shields.io 2021-07-14 01:36:25 +08:00
LouisLam
b6b7835d7e update to 1.0.3 2021-07-13 23:34:33 +08:00
LouisLam
d4fe5908f5 fix merging problem 2021-07-13 23:29:40 +08:00
LouisLam
af838d62e8 update 1.0.2 2021-07-13 23:09:12 +08:00
LouisLam
5a6e83b777 remove debug msg 2021-07-13 23:05:52 +08:00
LouisLam
c81930cacc add build-docker-nightly script 2021-07-13 23:03:55 +08:00
LouisLam
6d4694da43 add version-global-replace.js 2021-07-13 22:58:30 +08:00
LouisLam
9c23cd09ce use bcrypt for password hash 2021-07-13 22:22:46 +08:00
LouisLam
a60bf1528a drop ie support when build the frontend 2021-07-13 18:34:09 +08:00
LouisLam
1f3b337806 reset auto increment for new users 2021-07-13 18:21:06 +08:00
LouisLam
010ebea210 show version in the footer 2021-07-13 18:08:12 +08:00
LouisLam
b3a5d868a7 catch timezone error if browser do not have 2021-07-13 17:46:39 +08:00
LouisLam
312dec7393 add png icon 2021-07-13 12:38:59 +08:00
LouisLam
be1ef24cce add a comment 2021-07-13 12:24:33 +08:00
Louis Lam
c5de82b220 Merge pull request #35 from louislam/revert-32-feature/darkmode
Revert "basic darkmode"
2021-07-13 12:16:24 +08:00
Louis Lam
fef41b44a8 Revert "basic darkmode" 2021-07-13 12:16:11 +08:00
Louis Lam
6af65b688d Merge pull request #32 from philippdormann/feature/darkmode
basic darkmode
2021-07-13 11:58:57 +08:00
LouisLam
3e4a98b6bc Merge branch 'dev'
# Conflicts:
#	server/notification.js
2021-07-13 11:42:51 +08:00
LouisLam
866bf56319 add build-docker-nightly script 2021-07-13 11:32:40 +08:00
LouisLam
99afdabcac change the docker base image to node:14-alpine3.14, reduce the container size 2021-07-13 11:32:09 +08:00
LouisLam
0f1a95fde9 smtp without username password 2021-07-13 11:01:02 +08:00
LouisLam
edbab8163e update .editorconfig 2021-07-13 10:31:31 +08:00
LouisLam
551d00fc24 add some comments and remove traefik-network from docker-composer.yml 2021-07-13 10:28:07 +08:00
Louis Lam
3e4cdbecf2 Merge pull request #28 from yatadev/master
Create docker-compose.yml
2021-07-13 10:21:21 +08:00
Louis Lam
622681470d Merge pull request #22 from TheGuyDanish/master
Discord notification rework
2021-07-13 10:20:47 +08:00
Philipp Dormann
d9e2c230bf Merge branch 'master' of philippdormann/uptime-kuma into philippdormann/uptime-kuma->feature/darkmode
darkmode based on css variables. ref https://github.com/louislam/uptime-kuma/issues/21
2021-07-12 22:21:19 +02:00
Philipp Dormann
010302395f clean, multistage Dockerfile 2021-07-12 22:11:47 +02:00
jacr13
e053ee6573 fix bad pasting 2021-07-12 22:10:26 +02:00
Philipp Dormann
c4bc95927f dependency bumps 2021-07-12 22:09:27 +02:00
jacr13
3e305b79b2 remove debub console log 2021-07-12 22:08:42 +02:00
jacr13
c6237277c0 add support for signal notifications 2021-07-12 22:06:03 +02:00
Philipp Dormann
900219deb1 Merge remote-tracking branch 'theguydanish/master'
# Conflicts:
#	package-lock.json
#	package.json
2021-07-12 21:58:13 +02:00
Philipp Dormann
7ebeee3455 README: add sample docker-compose link
ref https://github.com/louislam/uptime-kuma/issues/25
2021-07-12 21:53:49 +02:00
Philipp Dormann
0abd3b2d16 README cleanup 2021-07-12 21:53:28 +02:00
Philipp Dormann
f452bf6b13 properly name Dockerfile 2021-07-12 21:50:51 +02:00
Philipp Dormann
8cd90d1e96 🐳 Docker 2021-07-12 21:50:39 +02:00
Philipp Dormann
789094a2ee formatting socket.js + deal with broken windows ports - default :50013 2021-07-12 21:49:18 +02:00
Philipp Dormann
5515437eab 🧹 cleanup 2021-07-12 21:47:32 +02:00
Philipp Dormann
7acb347012 🧹 fix formatting in server.js 2021-07-12 21:43:31 +02:00
Philipp Dormann
9d57e93367 Merge branch 'master' of https://github.com/philippdormann/uptime-kuma 2021-07-12 21:41:01 +02:00
Philipp Dormann
dae92d0ae4 Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	package.json
2021-07-12 21:40:35 +02:00
LouisLam
1259ff5368 smtp username/password is not required 2021-07-13 01:02:50 +08:00
yatadev
8debce82b1 Create docker-compose.yml 2021-07-12 18:23:38 +02:00
LouisLam
11a2adcb7c Merge remote-tracking branch 'origin/master' 2021-07-12 23:29:13 +08:00
LouisLam
ad615d1a90 remove some timezones which may cause error 2021-07-12 23:28:56 +08:00
TheGuyDanish
613c42b6d8 Discord revamp! Changed from bot to webhook, removed discord.js dep 2021-07-12 14:13:36 +01:00
Louis Lam
a6e16116f2 improve the docker script 2021-07-12 20:08:51 +08:00
Louis Lam
c7dfb36349 Update README.md 2021-07-12 20:00:12 +08:00
LouisLam
459dde2761 update the setup script to 1.0.1 2021-07-12 19:03:25 +08:00
LouisLam
cb94ab3bb5 add update guide 2021-07-12 18:59:48 +08:00
Philipp Dormann
763d7f2683 Merge branch 'louislam:master' into master 2021-07-12 12:01:00 +02:00
Philipp Dormann
e4f38d833d 🌑 darkmode support for nav link hover 2021-07-12 00:41:28 +02:00
Philipp Dormann
8b83266b00 🌑 add darkmode support for focused input elements 2021-07-12 00:37:08 +02:00
Philipp Dormann
6fb1b344f6 🌑 darkmode support on form elements 2021-07-12 00:33:52 +02:00
Philipp Dormann
b15b44e290 🐳 move Dockerfile to base node:alpine image
reduces size from about 1.08GB to 345MB (still not great but hey)
2021-07-12 00:28:11 +02:00
Philipp Dormann
66d991bd05 🐞 added missing v-bind:key to Dashboard 2021-07-12 00:27:29 +02:00
Philipp Dormann
673d3c124c 🚧 WIP on darkmode 🌑 2021-07-12 00:26:33 +02:00
Philipp Dormann
e568cad22c dependency bump + version pin 2021-07-12 00:25:22 +02:00
87 changed files with 22097 additions and 5220 deletions

View File

@@ -1,11 +0,0 @@
spec:
name: uptime-kuma
services:
- name: server
git:
repo_clone_url: https://github.com/louislam/uptime-kuma
branch: master
http_port: 3001
build_command: npm run setup
run_command: npm run start-server

View File

@@ -1,4 +1,37 @@
/.idea
/dist
/node_modules
/data/kuma.db
/data
/.do
**/.dockerignore
**/.git
**/.gitignore
**/docker-compose*
**/[Dd]ockerfile*
LICENSE
README.md
.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

@@ -13,3 +13,9 @@ trim_trailing_whitespace = false
[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2
[*.vue]
trim_trailing_whitespace = false

75
.eslintrc.js Normal file
View File

@@ -0,0 +1,75 @@
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,
}],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"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",
"array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"],
//'prefer-template': 'error',
"comma-dangle": ["warn", "only-multiline"],
},
}

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

@@ -0,0 +1,17 @@
---
name: Ask for help
about: You can ask any question related to Uptime Kuma.
title: ''
labels: help
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Info**
Uptime Kuma Version:
Using Docker?: Yes/No
OS:
Browser:

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

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**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.
**Info**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error Log**
It is easier for us to find out the problem.

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**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/.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

@@ -1,5 +1,7 @@
# Uptime Kuma
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a>
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
@@ -8,55 +10,57 @@ It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
# Features
## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping.
* 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.
# How to Use
## 🔧 How to Install
### 🚀 Installer via cli
Interactive cli installer, supports Docker or without Docker.
### Docker
```bash
docker run -d --restart=always -p 3001:3001 louislam/uptime-kuma
curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
```
### 🐳 Docker
```bash
docker volume create uptime-kuma
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
Browse to http://localhost:3001 after started.
Change Port and Volume
### Advanced Installation
```bash
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data louislam/uptime-kuma
```
If you need more options or need to browse via a reserve proxy, please read:
### Without Docker
Required Tools: Node.js >= 14, git and pm2.
```bash
git clone https://github.com/louislam/uptime-kuma.git
cd uptime-kuma
npm run setup
# Option 1. Try it
npm run start-server
# (Recommended)
# Option 2. Run in background using PM2
# Install PM2 if you don't have: npm install pm2 -g
pm2 start npm --name uptime-kuma -- run start-server
```
Browse to http://localhost:3001 after started.
### One-click Deploy to DigitalOcean
[![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)
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
# More Screenshots
## 🆙 How to Update
Please read:
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
## 🆕 What's Next?
I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
## 🖼 More Screenshots
Dark Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
Settings Page:
@@ -66,16 +70,21 @@ Telegram Notification Sample:
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
## 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.
* 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.
* Deploy my first Docker image to Docker Hub.
If you love this project, please consider giving me a ⭐.
## Contribute
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/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.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
https://github.com/louislam/uptime-kuma/issues

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;

74
db/patch6.sql Normal file
View File

@@ -0,0 +1,74 @@
-- 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,
maxredirects INTEGER default 10 not null,
accepted_statuscodes_json TEXT default '["200-299"]' not null
);
insert into
monitor_dg_tmp(
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
keyword,
maxretries,
ignore_tls,
upside_down
)
select
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
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;

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
# Simple docker-composer.yml
# You can change your port or volume location
version: '3.3'
services:
uptime-kuma:
image: louislam/uptime-kuma
container_name: uptime-kuma
volumes:
- ./uptime-kuma:/app/data
ports:
- 3001:3001

View File

@@ -1,10 +1,28 @@
FROM node:14
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS release
WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
ln -s /usr/bin/python3 /usr/bin/python && \
npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \
apk del .build-deps && \
rm -f /usr/bin/python
# Touching above code may causes sqlite3 re-compile again, painful slow.
# Install apprise
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
RUN pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
COPY . .
RUN npm install
RUN npm run build
RUN npm install && npm run build && npm prune
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
CMD ["npm", "run", "start-server"]
FROM release AS nightly
RUN npm run mark-as-nightly

View File

@@ -0,0 +1,2 @@
# Must enable File Sharing in Docker Desktop
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh

19
extra/healthcheck.js Normal file
View File

@@ -0,0 +1,19 @@
let http = require("http");
let options = {
host: "localhost",
port: "3001",
timeout: 2000,
};
let request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on("error", function (err) {
console.log("ERROR");
process.exit(1);
});
request.end();

245
extra/install.batsh Normal file
View File

@@ -0,0 +1,245 @@
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
// "npm run compile-install-script" to compile install.sh
// The command is working on Windows PowerShell and Docker for Windows only.
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
println("=====================");
println("Uptime Kuma Installer");
println("=====================");
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
println("---------------------------------------");
println("This script is designed for Linux and basic usage.");
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
println("---------------------------------------");
println("");
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
println("Docker - Install Uptime Kuma Docker container");
println("");
if ("$1" != "") {
type = "$1";
} else {
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
}
defaultPort = "3001";
function checkNode() {
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
println("Node Version: " ++ nodeVersion);
if (nodeVersion < "12") {
println("Error: Required Node.js 14");
call("exit", "1");
}
if (nodeVersion == "12") {
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
}
}
function deb() {
bash("nodeCheck=$(node -v)");
bash("apt --yes update");
if (nodeCheck != "") {
checkNode();
} else {
// Old nodejs binary name is "nodejs"
bash("check=$(nodejs --version)");
if (check != "") {
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
bash("exit 1");
}
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("apt --yes install curl");
}
println("Installing Node.js 14");
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
bash("apt --yes install nodejs");
bash("node -v");
bash("nodeCheckAgain=$(node -v)");
if (nodeCheckAgain == "") {
println("Error during Node.js installation");
bash("exit 1");
}
}
bash("check=$(git --version)");
if (check == "") {
println("Installing Git");
bash("apt --yes install git");
}
}
if (type == "local") {
defaultInstallPath = "/opt/uptime-kuma";
if (exists("/etc/redhat-release")) {
os = call("cat", "/etc/redhat-release");
distribution = "rhel";
} else if (exists("/etc/issue")) {
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
if (os == "Ubuntu") {
distribution = "ubuntu";
}
if (os == "Debian") {
distribution = "debian";
}
}
bash("arch=$(uname -i)");
println("Your OS: " ++ os);
println("Distribution: " ++ distribution);
println("Arch: " ++ arch);
if ("$3" != "") {
port = "$3";
} else {
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
if (port == "") {
port = defaultPort;
}
}
if ("$2" != "") {
installPath = "$2";
} else {
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
if (installPath == "") {
installPath = defaultInstallPath;
}
}
// CentOS
if (distribution == "rhel") {
bash("nodeCheck=$(node -v)");
if (nodeCheck != "") {
checkNode();
} else {
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("yum -y -q install curl");
}
println("Installing Node.js 14");
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
bash("yum install -y -q nodejs");
bash("node -v");
bash("nodeCheckAgain=$(node -v)");
if (nodeCheckAgain == "") {
println("Error during Node.js installation");
bash("exit 1");
}
}
bash("check=$(git --version)");
if (check == "") {
println("Installing Git");
bash("yum -y -q install git");
}
// Ubuntu
} else if (distribution == "ubuntu") {
deb();
// Debian
} else if (distribution == "debian") {
deb();
} else {
// Unknown distribution
error = 0;
bash("check=$(git --version)");
if (check == "") {
error = 1;
println("Error: git is missing");
}
bash("check=$(node -v)");
if (check == "") {
error = 1;
println("Error: node is missing");
}
if (error > 0) {
println("Please install above missing software");
bash("exit 1");
}
}
bash("check=$(pm2 --version)");
if (check == "") {
println("Installing PM2");
bash("npm install pm2 -g");
bash("pm2 startup");
}
bash("mkdir -p $installPath");
bash("cd $installPath");
bash("git clone https://github.com/louislam/uptime-kuma.git .");
bash("npm run setup");
bash("pm2 start npm --name uptime-kuma -- run start-server -- --port=$port");
} else {
defaultVolume = "uptime-kuma";
bash("check=$(docker -v)");
if (check == "") {
println("Error: docker is not found!");
bash("exit 1");
}
bash("check=$(docker info)");
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
echo \"Error: docker is not running\"
exit 1
fi");
if ("$3" != "") {
port = "$3";
} else {
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
if (port == "") {
port = defaultPort;
}
}
if ("$2" != "") {
volume = "$2";
} else {
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
if (volume == "") {
volume = defaultVolume;
}
}
println("Port: $port");
println("Volume: $volume");
bash("docker volume create $volume");
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
}
println("http://localhost:$port");

24
extra/mark-as-nightly.js Normal file
View File

@@ -0,0 +1,24 @@
const pkg = require("../package.json");
const fs = require("fs");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version
const newVersion = oldVersion + "-nightly"
console.log("Old Version: " + oldVersion)
console.log("New Version: " + newVersion)
if (newVersion) {
// Process package.json
pkg.version = newVersion
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
// Process README.md
if (fs.existsSync("README.md")) {
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
}
}

59
extra/reset-password.js Normal file
View File

@@ -0,0 +1,59 @@
console.log("== Uptime Kuma Reset Password Tool ==");
console.log("Loading the database");
const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
(async () => {
await Database.connect();
try {
const user = await R.findOne("user");
if (! user) {
throw new Error("user not found, have you installed?");
}
console.log("Found user: " + user.username);
while (true) {
let password = await question("New Password: ");
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
// Reset all sessions by reset jwt secret
await initJWTSecret();
rl.close();
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
} catch (e) {
console.error("Error: " + e.message);
}
await Database.close();
console.log("Finished. You should restart the Uptime Kuma server.")
})();
function question(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
})
});
}

62
extra/update-version.js Normal file
View File

@@ -0,0 +1,62 @@
const pkg = require("../package.json");
const fs = require("fs");
const child_process = require("child_process");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = process.argv[2];
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);
if (! newVersion) {
console.error("invalid version");
process.exit(1);
}
const exists = tagExists(newVersion);
if (! exists) {
// Process package.json
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Process README.md
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists")
}
function commit(version) {
let msg = "update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim();
console.log(stdout)
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error")
}
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
console.log(res.stdout.toString().trim())
}
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
return res.stdout.toString().trim() === version;
}

View File

@@ -1,13 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

203
install.sh Normal file
View File

@@ -0,0 +1,203 @@
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
# "npm run compile-install-script" to compile install.sh
# The command is working on Windows PowerShell and Docker for Windows only.
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
"echo" "-e" "====================="
"echo" "-e" "Uptime Kuma Installer"
"echo" "-e" "====================="
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
"echo" "-e" "---------------------------------------"
"echo" "-e" "This script is designed for Linux and basic usage."
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
"echo" "-e" "---------------------------------------"
"echo" "-e" ""
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
"echo" "-e" ""
if [ "$1" != "" ]; then
type="$1"
else
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
fi
defaultPort="3001"
function checkNode {
local _0
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
"echo" "-e" "Node Version: ""$nodeVersion"
_0="12"
if [ $(($nodeVersion < $_0)) == 1 ]; then
"echo" "-e" "Error: Required Node.js 14"
"exit" "1"
fi
if [ "$nodeVersion" == "12" ]; then
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
fi
}
function deb {
nodeCheck=$(node -v)
apt --yes update
if [ "$nodeCheck" != "" ]; then
"checkNode"
else
# Old nodejs binary name is "nodejs"
check=$(nodejs --version)
if [ "$check" != "" ]; then
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
exit 1
fi
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
apt --yes install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
apt --yes install nodejs
node -v
nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then
"echo" "-e" "Error during Node.js installation"
exit 1
fi
fi
check=$(git --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing Git"
apt --yes install git
fi
}
if [ "$type" == "local" ]; then
defaultInstallPath="/opt/uptime-kuma"
if [ -e "/etc/redhat-release" ]; then
os=$("cat" "/etc/redhat-release")
distribution="rhel"
else
if [ -e "/etc/issue" ]; then
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
if [ "$os" == "Ubuntu" ]; then
distribution="ubuntu"
fi
if [ "$os" == "Debian" ]; then
distribution="debian"
fi
fi
fi
arch=$(uname -i)
"echo" "-e" "Your OS: ""$os"
"echo" "-e" "Distribution: ""$distribution"
"echo" "-e" "Arch: ""$arch"
if [ "$3" != "" ]; then
port="$3"
else
"read" "-p" "Listening Port [$defaultPort]: " "port"
if [ "$port" == "" ]; then
port="$defaultPort"
fi
fi
if [ "$2" != "" ]; then
installPath="$2"
else
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
if [ "$installPath" == "" ]; then
installPath="$defaultInstallPath"
fi
fi
# CentOS
if [ "$distribution" == "rhel" ]; then
nodeCheck=$(node -v)
if [ "$nodeCheck" != "" ]; then
"checkNode"
else
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
yum -y -q install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
yum install -y -q nodejs
node -v
nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then
"echo" "-e" "Error during Node.js installation"
exit 1
fi
fi
check=$(git --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing Git"
yum -y -q install git
fi
# Ubuntu
else
if [ "$distribution" == "ubuntu" ]; then
"deb"
# Debian
else
if [ "$distribution" == "debian" ]; then
"deb"
else
# Unknown distribution
error=$((0))
check=$(git --version)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: git is missing"
fi
check=$(node -v)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: node is missing"
fi
if [ $(($error > 0)) == 1 ]; then
"echo" "-e" "Please install above missing software"
exit 1
fi
fi
fi
fi
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing PM2"
npm install pm2 -g
pm2 startup
fi
mkdir -p $installPath
cd $installPath
git clone https://github.com/louislam/uptime-kuma.git .
npm run setup
pm2 start npm --name uptime-kuma -- run start-server -- --port=$port
else
defaultVolume="uptime-kuma"
check=$(docker -v)
if [ "$check" == "" ]; then
"echo" "-e" "Error: docker is not found!"
exit 1
fi
check=$(docker info)
if [[ "$check" == *"Is the docker daemon running"* ]]; then
echo "Error: docker is not running"
exit 1
fi
if [ "$3" != "" ]; then
port="$3"
else
"read" "-p" "Expose Port [$defaultPort]: " "port"
if [ "$port" == "" ]; then
port="$defaultPort"
fi
fi
if [ "$2" != "" ]; then
volume="$2"
else
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
if [ "$volume" == "" ]; then
volume="$defaultVolume"
fi
fi
"echo" "-e" "Port: $port"
"echo" "-e" "Volume: $volume"
docker volume create $volume
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
fi
"echo" "-e" "http://localhost:$port"

19673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,86 @@
{
"name": "uptime-kuma",
"version": "1.3.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": "14.*"
},
"scripts": {
"dev": "vite --host",
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-demo-server": "set NODE_ENV=demo && node server/server.js",
"update": "",
"build": "vite build",
"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 . --push",
"setup": "git checkout 1.0.0 && npm install && npm run build"
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.3.0 --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.3.0 && npm install && npm run build",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile ."
},
"dependencies": {
"@popperjs/core": "^2.9.2",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"@louislam/sqlite3": "^5.0.3",
"@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0",
"axios": "^0.21.1",
"bootstrap": "^5.0.0",
"dayjs": "^1.10.4",
"discord.js": "^12.5.3",
"bcrypt": "^5.0.1",
"bootstrap": "^5.1.0",
"chart.js": "^3.5.0",
"chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9",
"dayjs": "^1.10.6",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.3",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.2",
"nodemailer": "^6.6.3",
"password-hash": "^1.2.2",
"redbean-node": "0.0.20",
"socket.io": "^4.0.2",
"socket.io-client": "^4.1.2",
"prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.21",
"socket.io": "^4.1.3",
"socket.io-client": "^4.1.3",
"tcp-ping": "^0.1.1",
"vue": "^3.0.5",
"v-pagination-3": "^0.1.6",
"vue": "^3.2.2",
"vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1"
},
"devDependencies": {
"@vitejs/plugin-legacy": "^1.4.3",
"@vitejs/plugin-vue": "^1.2.3",
"@vue/compiler-sfc": "^3.0.5",
"core-js": "^3.15.2",
"sass": "^1.35.1",
"vite": "^2.3.7"
"@babel/eslint-parser": "^7.15.0",
"@types/bootstrap": "^5.1.1",
"@vitejs/plugin-legacy": "^1.5.1",
"@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.2.2",
"core-js": "^3.16.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.16.0",
"sass": "^1.37.5",
"stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.3.5",
"vite": "^2.4.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 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,
});

151
server/database.js Normal file
View File

@@ -0,0 +1,151 @@
const fs = require("fs");
const { sleep } = require("../src/util");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const knex = require("knex");
const sqlite3 = require("@louislam/sqlite3");
class Database {
static templatePath = "./db/kuma.db"
static path = "./data/kuma.db";
static latestVersion = 6;
static noReject = true;
static sqliteInstance = null;
static async connect() {
if (! this.sqliteInstance) {
this.sqliteInstance = new sqlite3.Database(Database.path);
this.sqliteInstance.run("PRAGMA journal_mode = WAL");
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => sqlite3;
// Disable Pool by overriding acquireConnection()
Dialect.prototype.acquireConnection = async () => {
return this.sqliteInstance;
}
Dialect.prototype.releaseConnection = async () => { }
const knexInstance = knex({
client: Dialect,
connection: {
filename: Database.path,
},
useNullAsDefault: true,
});
R.setup(knexInstance);
if (process.env.SQL_LOG === "1") {
R.debug(true);
}
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
}
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 if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = "./data/kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
fs.copyFileSync(walPath, walPath + ".bak" + version);
}
// 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() {
if (this.sqliteInstance) {
this.sqliteInstance.close();
}
console.log("Stopped database");
}
}
module.exports = Database;

View File

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

View File

@@ -1,28 +1,31 @@
const https = require("https");
const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone')
const utc = require("dayjs/plugin/utc")
let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const {tcping, ping} = require("../util-server");
const {R} = require("redbean-node");
const {BeanModel} = require("redbean-node/dist/bean-model");
const {Notification} = require("../notification")
const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification")
const version = require("../../package.json").version;
/**
* status:
* 0 = DOWN
* 1 = UP
* 2 = PENDING
*/
class Monitor extends BeanModel {
async toJSON() {
let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id
this.id,
])
for (let bean of list) {
@@ -35,35 +38,72 @@ class Monitor extends BeanModel {
url: this.url,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: this.active,
type: this.type,
interval: this.interval,
keyword: this.keyword,
notificationIDList
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
notificationIDList,
};
}
/**
* Parse to boolean
* @returns {boolean}
*/
getIgnoreTls() {
return Boolean(this.ignoreTls)
}
/**
* Parse to boolean
* @returns {boolean}
*/
isUpsideDown() {
return Boolean(this.upsideDown);
}
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) {
let previousBeat = null;
let retries = 0;
let prometheus = new Prometheus(this);
const beat = async () => {
console.log(`Monitor ${this.id}: Heartbeat`)
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id
this.id,
])
}
const isFirstBeat = !previousBeat;
let bean = R.dispense("heartbeat")
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.status = 0;
bean.status = DOWN;
if (this.isUpsideDown()) {
bean.status = flipStatus(bean.status);
}
// Duration
if (previousBeat) {
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
if (! isFirstBeat) {
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
} else {
bean.duration = 0;
}
@@ -71,14 +111,43 @@ class Monitor extends BeanModel {
try {
if (this.type === "http" || this.type === "keyword") {
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, {
headers: { 'User-Agent':'Uptime-Kuma' }
})
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0,
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
});
bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") {
try {
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) {
if (e.message !== "No TLS certificate in response") {
console.error(e.message)
}
}
}
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
if (this.type === "http") {
bean.status = 1;
bean.status = UP;
} else {
let data = res.data;
@@ -90,43 +159,77 @@ class Monitor extends BeanModel {
if (data.includes(this.keyword)) {
bean.msg += ", keyword is found"
bean.status = 1;
bean.status = UP;
} else {
throw new Error(bean.msg + ", but keyword is not found")
}
}
} else if (this.type === "port") {
bean.ping = await tcping(this.hostname, this.port);
bean.msg = ""
bean.status = 1;
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname);
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) {
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
if (! previousBeat || previousBeat.status !== bean.status) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeat.status === UP && bean.status === DOWN) ||
(previousBeat.status === DOWN && bean.status === UP) ||
(previousBeat.status === PENDING && bean.status === DOWN);
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
if (isImportant) {
bean.important = true;
// Do not send if first beat is UP
if (previousBeat || bean.status !== 1) {
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [
this.id
// Send only if the first beat is DOWN
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
this.id,
])
let promiseList = [];
let text;
if (bean.status === 1) {
if (bean.status === UP) {
text = "✅ Up"
} else {
text = "🔴 Down"
@@ -134,17 +237,29 @@ class Monitor extends BeanModel {
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for(let notification of notificationList) {
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
}
}
await Promise.all(promiseList);
}
} else {
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, tlsInfo)
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)
@@ -161,10 +276,44 @@ class Monitor extends BeanModel {
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<object>}
*/
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);
return checkCertificateResult;
}
static async sendStats(io, monitorID, userID) {
Monitor.sendAvgPing(24, io, monitorID, userID);
Monitor.sendUptime(24, io, monitorID, userID);
Monitor.sendUptime(24 * 30, io, monitorID, userID);
await Monitor.sendAvgPing(24, io, monitorID, userID);
await Monitor.sendUptime(24, io, monitorID, userID);
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
await Monitor.sendCertInfo(io, monitorID, userID);
}
/**
@@ -172,6 +321,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let avgPing = parseInt(await R.getCell(`
SELECT AVG(ping)
FROM heartbeat
@@ -179,12 +330,23 @@ class Monitor extends BeanModel {
AND ping IS NOT NULL
AND monitor_id = ? `, [
-duration,
monitorID
monitorID,
]));
timeLogger.print(`[Monitor: ${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
* Calculation based on:
@@ -192,6 +354,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let sec = duration * 3600;
let heartbeatList = await R.getAll(`
@@ -200,9 +364,11 @@ class Monitor extends BeanModel {
WHERE time > DATETIME('now', ? || ' hours')
AND monitor_id = ? `, [
-duration,
monitorID
monitorID,
]);
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
let downtime = 0;
let total = 0;
let uptime;
@@ -224,7 +390,7 @@ class Monitor extends BeanModel {
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), 'second');
let trim = dayjs.utc().diff(dayjs(time), "second");
value = sec - trim;
if (value < 0) {
@@ -233,7 +399,7 @@ class Monitor extends BeanModel {
}
total += value;
if (row.status === 0) {
if (row.status === 0 || row.status === 2) {
downtime += value;
}
}
@@ -245,8 +411,6 @@ class Monitor extends BeanModel {
}
}
io.to(userID).emit("uptime", monitorID, duration, uptime);
}
}

21
server/model/user.js Normal file
View File

@@ -0,0 +1,21 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
class User extends BeanModel {
/**
* Direct execute, no need R.store()
* @param newPassword
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
this.id
]);
this.password = newPassword;
}
}
module.exports = User;

View File

@@ -1,28 +1,57 @@
const axios = require("axios");
const {R} = require("redbean-node");
const FormData = require('form-data');
const { R } = require("redbean-node");
const FormData = require("form-data");
const nodemailer = require("nodemailer");
const Discord = require('discord.js');
const child_process = require("child_process");
class Notification {
/**
*
* @param notification
* @param msg
* @param monitorJSON
* @param heartbeatJSON
* @returns {Promise<string>} Successful msg
* Throw Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
if (notification.type === "telegram") {
try {
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
}
},
})
return true;
return okMsg;
} catch (error) {
console.log(error)
return false;
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
}
} else if (notification.type === "gotify") {
try {
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "webhook") {
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
@@ -33,29 +62,380 @@ class Notification {
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append('data', JSON.stringify(data));
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders()
headers: finalData.getHeaders(),
}
} else {
finalData = data;
}
let res = await axios.post(notification.webhookURL, finalData, config)
return true;
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
console.log(error)
return false;
throwGeneralAxiosError(error)
}
} else if (notification.type === "smtp") {
return await Notification.smtp(notification, msg)
} else if (notification.type === "discord") {
return await Notification.discord(notification, msg)
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == 0) {
let discorddowndata = {
username: discordDisplayName,
embeds: [{
title: "❌ One of your services went down. ❌",
color: 16711680,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: monitorJSON["url"],
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Error",
value: heartbeatJSON["msg"],
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg;
} else if (heartbeatJSON["status"] == 1) {
let discordupdata = {
username: discordDisplayName,
embeds: [{
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
color: 65280,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: "[Visit Service](" + monitorJSON["url"] + ")",
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg;
}
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "signal") {
try {
let data = {
"message": msg,
"number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (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 === "octopush") {
try {
let config = {
headers: {
"api-key": notification.octopushAPIKey,
"api-login": notification.octopushLogin,
"cache-control": "no-cache"
}
};
let data = {
"recipients": [
{
"phone_number": notification.octopushPhoneNumber
}
],
//octopush not supporting non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": notification.octopushSMSType,
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "slack") {
try {
if (heartbeatJSON == null) {
let 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;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
},
{
"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",
},
],
}],
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "pushover") {
let pushoverlink = "https://api.pushover.net/1/messages.json"
try {
if (heartbeatJSON == null) {
let data = {
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
"user": notification.pushoveruserkey,
"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;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"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;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "apprise") {
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 if (notification.type === "pushbullet") {
try {
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
let config = {
headers: {
"Access-Token": notification.pushbulletAccessToken,
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
let testdata = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == 0) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == 1) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "line") {
try {
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text":"Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == 0) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text":"UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == 1) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text":"UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else {
throw new Error("Notification type is not supported")
}
@@ -99,38 +479,70 @@ class Notification {
static async smtp(notification, msg) {
let transporter = nodemailer.createTransport({
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
auth: {
};
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
},
});
};
}
let transporter = nodemailer.createTransport(config);
// send mail with defined transport object
let info = await transporter.sendMail({
await transporter.sendMail({
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo,
subject: msg,
text: msg,
});
return true;
return "Sent Successfully.";
}
static async discord(notification, msg) {
const client = new Discord.Client();
await client.login(notification.discordToken)
static async apprise(notification, msg) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
const channel = await client.channels.fetch(notification.discordChannelID);
await channel.send(msg);
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
client.destroy()
if (output) {
return true;
if (! output.includes("ERROR")) {
return "Sent Successfully";
}
throw new Error(output)
} else {
return ""
}
}
static checkApprise() {
let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise");
return exists;
}
}
function throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
}
}
throw new Error(msg)
}
module.exports = {

23
server/password-hash.js Normal file
View File

@@ -0,0 +1,23 @@
const passwordHashOld = require("password-hash");
const bcrypt = require("bcrypt");
const saltRounds = 10;
exports.generate = function (password) {
return bcrypt.hashSync(password, saltRounds);
}
exports.verify = function (password, hash) {
if (isSHA1(hash)) {
return passwordHashOld.verify(password, hash)
}
return bcrypt.compareSync(password, hash);
}
function isSHA1(hash) {
return (typeof hash === "string" && hash.startsWith("sha1"))
}
exports.needRehash = function (hash) {
return isSHA1(hash);
}

View File

@@ -1,18 +1,21 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows
var spawn = require('child_process').spawn,
events = require('events'),
fs = require('fs'),
const net = require("net");
const spawn = require("child_process").spawn,
events = require("events"),
fs = require("fs"),
WIN = /^win/.test(process.platform),
LIN = /^linux/.test(process.platform),
MAC = /^darwin/.test(process.platform);
FBSD = /^freebsd/.test(process.platform);
const { debug } = require("../src/util");
module.exports = Ping;
function Ping(host, options) {
if (!host)
throw new Error('You must specify a host to ping!');
if (!host) {
throw new Error("You must specify a host to ping!");
}
this._host = host;
this._options = options = (options || {});
@@ -20,26 +23,52 @@ function Ping(host, options) {
events.EventEmitter.call(this);
if (WIN) {
this._bin = 'c:/windows/system32/ping.exe';
this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ];
this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
this._regmatch = /[><=]([0-9.]+?)ms/;
}
else if (LIN) {
this._bin = '/bin/ping';
this._args = (options.args) ? options.args : [ '-n', '-w', '2', '-c', '1', host ];
this._regmatch = /=([0-9.]+?) ms/; // need to verify this
}
else if (MAC) {
this._bin = '/sbin/ping';
this._args = (options.args) ? options.args : [ '-n', '-t', '2', '-c', '1', host ];
} else if (LIN) {
this._bin = "/bin/ping";
const defaultArgs = [ "-n", "-w", "2", "-c", "1", host ];
if (net.isIPv6(host) || options.ipv6) {
defaultArgs.unshift("-6");
}
this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/;
}
else {
throw new Error('Could not detect your ping binary.');
} else if (MAC) {
if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6";
} else {
this._bin = "/sbin/ping";
}
this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/;
} else if (FBSD) {
this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", "2", "-c", "1", host ];
if (net.isIPv6(host) || options.ipv6) {
defaultArgs.unshift("-6");
}
this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/;
} else {
throw new Error("Could not detect your ping binary.");
}
if (!fs.existsSync(this._bin))
throw new Error('Could not detect '+this._bin+' on your system');
if (!fs.existsSync(this._bin)) {
throw new Error("Could not detect " + this._bin + " on your system");
}
this._i = 0;
@@ -50,62 +79,71 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING
// ===========
Ping.prototype.send = function(callback) {
var self = this;
callback = callback || function(err, ms) {
if (err) return self.emit('error', err);
else return self.emit('result', ms);
Ping.prototype.send = function (callback) {
let self = this;
callback = callback || function (err, ms) {
if (err) {
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.on('error', function(err) { // handle binary errors
this._ping.on("error", function (err) { // handle binary errors
_errored = true;
callback(err);
});
this._ping.stdout.on('data', function(data) { // log stdout
this._stdout = (this._stdout || '') + data;
this._ping.stdout.on("data", function (data) { // log stdout
this._stdout = (this._stdout || "") + data;
});
this._ping.stdout.on('end', function() {
this._ping.stdout.on("end", function () {
_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._stderr = (this._stderr || '') + data;
this._ping.stderr.on("data", function (data) { // log stderr
this._stderr = (this._stderr || "") + data;
});
this._ping.on('exit', function(code) { // handle complete
this._ping.on("exit", function (code) { // handle complete
_exited = true;
if (_ended && !_errored) onEnd.call(self._ping);
if (_ended && !_errored) {
onEnd.call(self._ping);
}
});
function onEnd() {
var stdout = this.stdout._stdout,
let stdout = this.stdout._stdout,
stderr = this.stderr._stderr,
ms;
if (stderr)
if (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 = (ms && ms[1]) ? Number(ms[1]) : ms;
callback(null, ms);
callback(null, ms, stdout);
}
};
// CALL Ping#send(callback) ON A TIMER
// ===================================
Ping.prototype.start = function(callback) {
var self = this;
this._i = setInterval(function() {
Ping.prototype.start = function (callback) {
let self = this;
this._i = setInterval(function () {
self.send(callback);
}, (self._options.interval || 5000));
self.send(callback);
@@ -113,6 +151,6 @@ Ping.prototype.start = function(callback) {
// STOP SENDING PINGS
// ==================
Ping.prototype.stop = function() {
Ping.prototype.stop = function () {
clearInterval(this._i);
};

90
server/prometheus.js Normal file
View File

@@ -0,0 +1,90 @@
const PrometheusClient = require("prom-client");
const commonLabels = [
"monitor_name",
"monitor_type",
"monitor_url",
"monitor_hostname",
"monitor_port",
]
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires",
labelNames: commonLabels
});
const monitor_cert_is_valid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0= No)",
labelNames: commonLabels
});
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, tlsInfo) {
if (typeof tlsInfo !== "undefined") {
try {
let is_valid = 0
if (tlsInfo.valid == true) {
is_valid = 1
} else {
is_valid = 0
}
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid)
} catch (e) {
console.error(e)
}
try {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
} catch (e) {
console.error(e)
}
}
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,50 +1,127 @@
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
const dayjs = require("dayjs");
const {R} = require("redbean-node");
const passwordHash = require('password-hash');
const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor");
console.log("Welcome to Uptime Kuma");
console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
console.log("Importing Node libraries")
const fs = require("fs");
const {getSettings} = require("./util-server");
const {Notification} = require("./notification")
const args = require('args-parser')(process.argv);
const http = require("http");
console.log("args:")
console.log(args)
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");
const hostname = args.host || "0.0.0.0"
const port = args.port || 3001
console.log("Importing this project modules");
debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret } = 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;
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
const hostname = process.env.HOST || args.host;
const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version)
console.log("Creating express and socket.io instance")
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.json())
/**
* Total WebSocket client connected to server currently, no actual use
* @type {number}
*/
let totalClient = 0;
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Main monitor list
* @type {{}}
*/
let monitorList = {};
/**
* Show Setup Page
* @type {boolean}
*/
let needSetup = false;
/**
* Cache Index HTML
* @type {string}
*/
let indexHTML = fs.readFileSync("./dist/index.html").toString();
(async () => {
await initDatabase();
app.use('/', express.static("dist"));
console.log("Adding route")
app.post('/test-webhook', function(request, response, next) {
console.log("Test Webhook (application/json only)")
console.log("Content-Type: " + request.header("Content-Type"))
console.log(request.body)
response.end();
// Normal Router here
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (! await setting("searchEngineIndex")) {
txt += " /";
}
response.setHeader("Content-Type", "text/plain");
response.send(txt);
});
app.get('*', function(request, response, next) {
response.sendFile(process.cwd() + '/dist/index.html');
// Basic Auth Router here
// Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
// Universal Route Handler, must be at the end
app.get("*", async (_request, response) => {
response.send(indexHTML);
});
io.on('connection', async (socket) => {
console.log('a user connected');
console.log("Adding socket handler")
io.on("connection", async (socket) => {
socket.emit("info", {
version,
})
totalClient++;
if (needSetup) {
@@ -52,12 +129,13 @@ let needSetup = false;
socket.emit("setup")
}
socket.on('disconnect', () => {
console.log('user disconnected');
socket.on("disconnect", () => {
totalClient--;
});
// ***************************
// Public API
// ***************************
socket.on("loginByToken", async (token, callback) => {
@@ -67,25 +145,29 @@ let needSetup = false;
console.log("Username from JWT: " + decoded.username)
let user = await R.findOne("user", " username = ? AND active = 1 ", [
decoded.username
decoded.username,
])
if (user) {
debug("afterLogin")
await afterLogin(socket, user)
debug("afterLogin ok")
callback({
ok: true,
})
} else {
callback({
ok: false,
msg: "The user is inactive or deleted."
msg: "The user is inactive or deleted.",
})
}
} catch (error) {
callback({
ok: false,
msg: "Invalid token."
msg: "Invalid token.",
})
}
@@ -94,24 +176,21 @@ let needSetup = false;
socket.on("login", async (data, callback) => {
console.log("Login")
let user = await R.findOne("user", " username = ? AND active = 1 ", [
data.username
])
if (user && passwordHash.verify(data.password, user.password)) {
let user = await login(data.username, data.password)
if (user) {
await afterLogin(socket, user)
callback({
ok: true,
token: jwt.sign({
username: data.username
}, jwtSecret)
username: data.username,
}, jwtSecret),
})
} else {
callback({
ok: false,
msg: "Incorrect username or password."
msg: "Incorrect username or password.",
})
}
@@ -142,23 +221,22 @@ let needSetup = false;
callback({
ok: true,
msg: "Added Successfully."
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
// ***************************
// Auth Only API
// ***************************
// Add a new monitor
socket.on("add", async (monitor, callback) => {
try {
checkLogin(socket)
@@ -167,6 +245,9 @@ let needSetup = false;
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor)
bean.user_id = socket.userID
await R.store(bean)
@@ -179,17 +260,18 @@ let needSetup = false;
callback({
ok: true,
msg: "Added Successfully.",
monitorID: bean.id
monitorID: bean.id,
});
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
// Edit a monitor
socket.on("editMonitor", async (monitor, callback) => {
try {
checkLogin(socket)
@@ -205,8 +287,13 @@ let needSetup = false;
bean.url = monitor.url
bean.interval = monitor.interval
bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries;
bean.port = monitor.port;
bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
await R.store(bean)
@@ -221,14 +308,14 @@ let needSetup = false;
callback({
ok: true,
msg: "Saved.",
monitorID: bean.id
monitorID: bean.id,
});
} catch (e) {
console.log(e)
console.error(e)
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -252,7 +339,7 @@ let needSetup = false;
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -266,13 +353,13 @@ let needSetup = false;
callback({
ok: true,
msg: "Resumed Successfully."
msg: "Resumed Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -285,14 +372,13 @@ let needSetup = false;
callback({
ok: true,
msg: "Paused Successfully."
msg: "Paused Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -310,12 +396,12 @@ let needSetup = false;
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID,
socket.userID
socket.userID,
]);
callback({
ok: true,
msg: "Deleted Successfully."
msg: "Deleted Successfully.",
});
await sendMonitorList(socket);
@@ -323,7 +409,7 @@ let needSetup = false;
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -337,19 +423,16 @@ let needSetup = false;
}
let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID
socket.userID,
])
if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password.newPassword),
socket.userID
]);
user.resetPassword(password.newPassword);
callback({
ok: true,
msg: "Password has been updated successfully."
msg: "Password has been updated successfully.",
})
} else {
throw new Error("Incorrect current password")
@@ -358,25 +441,43 @@ let needSetup = false;
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
socket.on("getSettings", async (type, callback) => {
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket)
callback({
ok: true,
data: await getSettings(type),
data: await getSettings("general"),
});
} catch (e) {
callback({
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,
});
}
});
@@ -397,7 +498,7 @@ let needSetup = false;
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -417,7 +518,7 @@ let needSetup = false;
} catch (e) {
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
@@ -426,33 +527,70 @@ let needSetup = false;
try {
checkLogin(socket)
await Notification.send(notification, notification.name + " Testing")
let msg = await Notification.send(notification, notification.name + " Testing")
callback({
ok: true,
msg: "Sent Successfully"
msg,
});
} catch (e) {
console.error(e)
callback({
ok: false,
msg: e.message
msg: e.message,
});
}
});
socket.on("checkApprise", async (callback) => {
try {
checkLogin(socket)
callback(Notification.checkApprise());
} catch (e) {
callback(false);
}
});
debug("added all socket handlers")
// ***************************
// Better do anything after added all socket handlers here
// ***************************
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 the server")
server.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
await Database.close();
});
server.listen(port, hostname, () => {
console.log(`Listening on ${hostname}:${port}`);
if (hostname) {
console.log(`Listening on ${hostname}:${port}`);
} else {
console.log(`Listening on ${port}`);
}
startMonitors();
});
})();
async function updateMonitorNotification(monitorID, notificationIDList) {
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID,
])
for (let notificationID in notificationIDList) {
@@ -485,7 +623,7 @@ async function sendMonitorList(socket) {
async function sendNotificationList(socket) {
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID
socket.userID,
]);
for (let bean of list) {
@@ -502,20 +640,24 @@ async function afterLogin(socket, user) {
let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
await sendImportantHeartbeatList(socket, monitorID);
await Monitor.sendStats(io, monitorID, user.id)
}
sendNotificationList(socket)
await sendNotificationList(socket)
// Delay a bit, so that it let the main page to query the data first, since SQLite can process one sql at the same time only.
// For example, query the edit data first.
setTimeout(async () => {
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
}
}, 500);
}
async function getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [
userID
userID,
])
for (let monitor of monitorList) {
@@ -532,36 +674,31 @@ function checkLogin(socket) {
}
async function initDatabase() {
const path = './data/kuma.db';
if (! fs.existsSync(path)) {
console.log("Copy Database")
fs.copyFileSync("./db/kuma.db", path);
if (! fs.existsSync(Database.path)) {
console.log("Copying Database")
fs.copyFileSync(Database.templatePath, Database.path);
}
console.log("Connect to Database")
console.log("Connecting to Database")
await Database.connect();
console.log("Connected")
R.setup('sqlite', {
filename: path
});
R.freeze(true)
await R.autoloadModels("./server/model");
// Patch the database
await Database.patch()
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret"
"jwtSecret",
]);
if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.")
jwtSecretBean = R.dispense("setting")
jwtSecretBean.key = "jwtSecret"
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
await R.store(jwtSecretBean)
console.log("JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret();
console.log("Stored JWT secret into database");
} else {
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) {
console.log("No user, need setup")
needSetup = true;
@@ -577,11 +714,11 @@ async function startMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
monitorID,
userID
userID,
]);
let monitor = await R.findOne("monitor", " id = ? ", [
monitorID
monitorID,
])
if (monitor.id in monitorList) {
@@ -603,7 +740,7 @@ async function pauseMonitor(userID, monitorID) {
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
monitorID,
userID
userID,
]);
if (monitorID in monitorList) {
@@ -618,41 +755,84 @@ async function startMonitors() {
let list = await R.find("monitor", " active = 1 ")
for (let monitor of list) {
monitor.start(io)
monitorList[monitor.id] = monitor;
}
for (let monitor of list) {
monitor.start(io);
// Give some delays, so all monitors won't make request at the same moment when just start the server.
await sleep(getRandomInt(300, 1000));
}
}
/**
* Send Heartbeat History list to socket
*/
async function sendHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID
monitorID,
])
let result = [];
for (let bean of list) {
result.unshift(bean.toJSON())
result.unshift(bean.toJSON())
}
socket.emit("heartbeatList", monitorID, result)
}
async function sendImportantHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT 500
`, [
monitorID
monitorID,
])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
socket.emit("importantHeartbeatList", monitorID, list)
}
async function shutdownFunction(signal) {
console.log("Shutdown requested");
console.log("Called signal: " + signal);
console.log("Stopping all monitors")
for (let id in monitorList) {
let monitor = monitorList[id]
monitor.stop()
}
await sleep(2000);
await Database.close();
}
function finalFunction() {
console.log("Graceful shutdown successfully!");
}
gracefulShutdown(server, {
signals: "SIGINT SIGTERM",
timeout: 30000, // timeout: 30 secs
development: false, // not in dev mode
forceExit: true, // triggers process.exit() at the end of shutdown process
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction, // finally function (sync) - e.g. for logging
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
console.trace(error);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});

View File

@@ -1,6 +1,28 @@
const tcpp = require('tcp-ping');
const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const {R} = require("redbean-node");
const { R } = require("redbean-node");
const { debug } = require("../src/util");
const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
*/
exports.initJWTSecret = async () => {
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);
if (! jwtSecretBean) {
jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret";
}
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
await R.store(jwtSecretBean);
return jwtSecretBean;
}
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
@@ -8,7 +30,7 @@ exports.tcping = function (hostname, port) {
address: hostname,
port: port,
attempts: 1,
}, function(err, data) {
}, function (err, data) {
if (err) {
reject(err);
@@ -23,15 +45,30 @@ exports.tcping = function (hostname, port) {
});
}
exports.ping = function (hostname) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname);
exports.ping = async (hostname) => {
try {
return await exports.pingAsync(hostname);
} catch (e) {
// If the host cannot be resolved, try again with ipv6
if (e.message.includes("service not known")) {
return await exports.pingAsync(hostname, true);
} else {
throw e;
}
}
}
ping.send(function(err, ms) {
exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
ipv6
});
ping.send(function (err, ms, stdout) {
if (err) {
reject(err)
reject(err);
} else if (ms === null) {
reject(new Error("timeout"))
reject(new Error(stdout))
} else {
resolve(Math.round(ms))
}
@@ -40,23 +77,149 @@ exports.ping = function (hostname) {
}
exports.setting = async function (key) {
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key
let value = await R.getCell("SELECT `value` FROM setting WHERE `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) {
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
type
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
])
let result = {};
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;
}
}
console.log(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,
};
}
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
// Param: accepted_codes - an array of accepted status codes
// Return: true if the status code is within the accepted ranges, false otherwise
// Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) {
if (accepted_codes == null || accepted_codes.length === 0) {
return false;
}
for (const code_range of accepted_codes) {
const code_range_split = code_range.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) {
if (status === code_range_split[0]) {
return true;
}
} else if (code_range_split.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) {
return true;
}
} else {
throw new Error("Invalid status code range");
}
}
return false;
}

View File

@@ -1,20 +0,0 @@
/*
* Common functions - can be used in frontend or backend
*/
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);
}

View File

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

View File

@@ -5,8 +5,45 @@
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
}
h1 {
font-size: 32px;
}
h2 {
font-size: 26px;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: #CCC;
border-radius: 20px;
}
.modal {
backdrop-filter: blur(3px);
}
.modal-content {
border-radius: 1rem;
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
.dark & {
box-shadow: 0 15px 70px rgb(0 0 0);
background-color: $dark-bg;
}
}
.VuePagination__count {
font-size: 13px;
text-align: center;
}
.shadow-box {
overflow: hidden;
//overflow: hidden; // Forget why add this, but multiple select hide by this
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
padding: 10px;
border-radius: 10px;
@@ -29,10 +66,137 @@
background-color: $highlight;
border-color: $highlight;
}
.dark & {
color: $dark-font-color2;
}
}
.modal-content {
border-radius: 1rem;
backdrop-filter: blur(3px);
// Dark Theme override here
.dark {
background-color: #090C10;
color: $dark-font-color;
&::-webkit-scrollbar-thumb {
background: $dark-border-color;
}
.shadow-box {
background-color: $dark-bg;
}
.form-check-input {
background-color: $dark-bg2;
}
.form-switch .form-check-input {
background-color: #131a21;
}
a,
.table,
.nav-link {
color: $dark-font-color;
}
.form-control,
.form-control:focus,
.form-select,
.form-select:focus {
color: $dark-font-color;
background-color: $dark-bg2;
}
.form-control, .form-select {
border-color: $dark-border-color;
}
.table-hover > tbody > tr:hover {
--bs-table-accent-bg: #070A10;
color: $dark-font-color;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: $dark-font-color2;
}
.bg-primary {
color: $dark-font-color2;
}
.btn-secondary {
color: white;
}
.btn-close {
box-shadow: none;
filter: invert(1);
&:hover {
opacity: 0.6;
}
}
.modal-header {
border-color: $dark-bg;
}
.modal-footer {
border-color: $dark-bg;
}
// Pagination
.page-item.disabled .page-link {
background-color: $dark-bg;
border-color: $dark-border-color;
}
.page-link {
background-color: $dark-bg;
border-color: $dark-border-color;
color: $dark-font-color;
}
// Multiselect
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input, .multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
}
/*
* Transitions
*/
// page-change
.slide-fade-enter-active {
transition: all 0.20s $easing-in;
}
.slide-fade-leave-active {
transition: all 0.20s $easing-in;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(50px);
opacity: 0;
}

View File

@@ -1,7 +1,18 @@
$primary: #5CDD8B;
$danger: #DC3545;
$warning: #f8a306;
$link-color: #111;
$border-radius: 50rem;
$highlight: #7ce8a4;
$highlight-white: #e7faec;
$dark-font-color: #b1b8c0;
$dark-font-color2: #020b05;
$dark-bg: #0D1117;
$dark-bg2: #070A10;
$dark-border-color: #1d2634;
$easing-in: cubic-bezier(0.54,0.78,0.55,0.97);
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,30 @@
<template>
<div class="wrap" :style="wrapStyle" ref="wrap">
<div ref="wrap" class="wrap" :style="wrapStyle">
<div class="hp-bar-big" :style="barStyle">
<div
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
:style="beatStyle"
v-for="(beat, index) in shortBeatList"
:key="index"
:title="beat.msg">
</div>
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:style="beatStyle"
:title="getBeatTitle(beat)"
/>
</div>
</div>
</template>
<script>
export default {
props: {
size: {
type: String,
default: "big"
default: "big",
},
monitorId: {
type: Number,
required: true,
},
monitorId: Number
},
data() {
return {
@@ -34,9 +36,92 @@ export default {
maxBeat: -1,
}
},
computed: {
beatList() {
return this.$root.heartbeatList[this.monitorId]
},
shortBeatList() {
if (! this.beatList) {
return [];
}
let placeholders = [];
let start = this.beatList.length - this.maxBeat;
if (this.move) {
start = start - 1;
}
if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
}
start = 0;
}
return placeholders.concat(this.beatList.slice(start))
},
wrapStyle() {
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
return {
padding: `${topBottom}px ${leftRight}px`,
width: "100%",
}
},
barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2);
return {
transition: "all ease-in-out 0.25s",
transform: `translateX(${width}px)`,
}
}
return {
transform: "translateX(0)",
}
},
beatStyle() {
return {
width: this.beatWidth + "px",
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
}
},
},
watch: {
beatList: {
handler(val, oldVal) {
this.move = true;
setTimeout(() => {
this.move = false;
}, 300)
},
deep: true,
},
},
unmounted() {
window.removeEventListener("resize", this.resize);
},
beforeMount() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
@@ -52,98 +137,16 @@ export default {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
},
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
}
},
computed: {
beatList() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
return this.$root.heartbeatList[this.monitorId]
},
shortBeatList() {
let placeholders = []
let start = this.beatList.length - this.maxBeat;
if (this.move) {
start = start - 1;
}
if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
}
start = 0;
}
return placeholders.concat(this.beatList.slice(start))
},
wrapStyle() {
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 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 {
padding: `${topBottom}px ${leftRight}px`,
width: width
}
},
barStyle() {
if (this.move && this.shortBeatList.length > this.maxBeat) {
let width = -(this.beatWidth + this.beatMargin * 2);
return {
transition: "all ease-in-out 0.25s",
transform: `translateX(${width}px)`,
}
} else {
return {
transform: `translateX(0)`,
}
}
},
beatStyle() {
return {
width: this.beatWidth + "px",
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
}
}
},
watch: {
beatList: {
handler(val, oldVal) {
this.move = true;
setTimeout(() => {
this.move = false;
}, 300)
},
deep: true,
}
}
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars.scss";
.wrap {
@@ -160,12 +163,20 @@ export default {
&.empty {
background-color: aliceblue;
.dark & {
background-color: #d0d3d5;
}
}
&.down {
background-color: $danger;
}
&.pending {
background-color: $warning;
}
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;
@@ -174,4 +185,10 @@ export default {
}
}
.dark {
.hp-bar-big .beat.empty{
background-color: #848484;
}
}
</style>

View File

@@ -2,31 +2,32 @@
<div class="form-container">
<div class="form">
<form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal"></h1>
<h1 class="h3 mb-3 fw-normal" />
<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>
</div>
<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>
</div>
<div class="form-check mb-3 mt-3" >
<label>
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check">
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember">
Remember me
</label>
</label>
</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 }}
</div>
</form>
@@ -52,8 +53,8 @@ export default {
this.processing = false;
this.res = res;
})
}
}
},
},
}
</script>

View File

@@ -0,0 +1,133 @@
<template>
<div class="shadow-box list mb-4">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
No Monitors, please <router-link to="/add">add one</router-link>.
</div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<div class="row">
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
export default {
components: {
Uptime,
HeartbeatBar,
},
computed: {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
})
return result;
},
},
methods: {
monitorURL(id) {
return "/dashboard/" + id;
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
.list {
height: auto;
min-height: calc(100vh - 240px);
.item {
display: block;
text-decoration: none;
padding: 13px 15px 10px 15px;
border-radius: 10px;
transition: all ease-in-out 0.15s;
&.disabled {
opacity: 0.3;
}
.info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
}
}
.dark {
.list {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
}
.monitorItem {
width: 100%;
}
</style>

View File

@@ -1,78 +1,94 @@
<template>
<form @submit.prevent="submit">
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<h5 id="exampleModalLabel" class="modal-title">
Setup Notification
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select id="type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option>
<option value="discord">Discord</option>
<option value="signal">Signal</option>
<option value="gotify">Gotify</option>
<option value="slack">Slack</option>
<option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
<option value="line">Line Messenger</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="notification.name" type="text" class="form-control" required>
</div>
<template v-if="notification.type === 'telegram'">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="notification.type">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option>
<option value="discord">Discord</option>
</select>
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<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>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="notification.name">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
Auto Get
</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
<template v-if="notification.type === 'telegram'">
<div class="mb-3">
<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">
<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 class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</template>
</template>
<template v-if="notification.type === 'webhook'">
<div class="mb-3">
<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 class="mb-3">
<label for="webhook-content-type" class="form-label">Content Type</label>
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required>
<option value="json">application/json</option>
<option value="form-data">multipart/form-data</option>
<select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
<option value="json">
application/json
</option>
<option value="form-data">
multipart/form-data
</option>
</select>
<div class="form-text">
@@ -85,92 +101,344 @@
<template v-if="notification.type === 'smtp'">
<div class="mb-3">
<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 class="mb-3">
<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 class="mb-3">
<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">
Secure
</label>
</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 class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required v-model="notification.smtpUsername" autocomplete="false">
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required v-model="notification.smtpPassword" autocomplete="false">
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<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 class="mb-3">
<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>
</template>
<template v-if="notification.type === 'discord'">
<div class="mb-3">
<label for="discord-token" class="form-label">Discord Bot Token</label>
<input type="text" class="form-control" id="discord-token" required v-model="notification.discordToken" autocomplete="false">
<div class="form-text">You should create a Discord app and create a bot from <a href="https://discord.com/developers/applications" target="_blank">here</a>.</div>
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
<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>
<div class="mb-3">
<label for="discordChannelID" class="form-label">Channel ID</label>
<input type="text" class="form-control" id="discordChannelID" required v-model="notification.discordChannelID" autocomplete="false">
<label for="discord-username" class="form-label">Bot Display Name</label>
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div>
</template>
<template v-if="notification.type === 'signal'">
<div class="mb-3">
<label for="signal-url" class="form-label">Post URL</label>
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-number" class="form-label">Number</label>
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="signal-recipients" class="form-label">Recipients</label>
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
<div class="form-text">
You should add the bot to your channel. <br />
<a href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-" target="_blank">Where can I find the channel id?</a><br />
<a href="https://discordapi.com/permissions.html#8" target="_blank">How to add a bot to your channel?</a>
You need to have a signal client with REST API.
<p style="margin-top: 8px;">
You can check this url to view how to setup one:
</p>
<p style="margin-top: 8px;">
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
</p>
<p style="margin-top: 8px;">
IMPORTANT: You cannot mix groups and numbers in recipients!
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'gotify'">
<div class="mb-3">
<label for="gotify-application-token" class="form-label">Application Token</label>
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">Server URL</label>
<div class="input-group mb-3">
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label for="gotify-priority" class="form-label">Priority</label>
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
</div>
</template>
<template v-if="notification.type === 'slack'">
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label>
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">Channel Name</label>
<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>
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
</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 === 'octopush'">
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<input id="octopush-key" v-model="notification.octopushAPIKey" type="text" class="form-control" required>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">SMS Type</label>
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
</select>
<div class="form-text">
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
</div>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</p>
</template>
<template v-if="notification.type === 'pushover'">
<div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
<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>
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label>
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
<label for="pushover-priority" class="form-label">Priority</label>
<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>
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
<option>pushover</option>
<option>bike</option>
<option>bugle</option>
<option>cashregister</option>
<option>classical</option>
<option>cosmic</option>
<option>falling</option>
<option>gamelan</option>
<option>incoming</option>
<option>intermission</option>
<option>mechanical</option>
<option>pianobar</option>
<option>siren</option>
<option>spacealarm</option>
<option>tugboat</option>
<option>alien</option>
<option>climb</option>
<option>persistent</option>
<option>echo</option>
<option>updown</option>
<option>vibrate</option>
<option>none</option>
</select>
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p>
<p style="margin-top: 8px;">
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
</p>
<p style="margin-top: 8px;">
If you want to send notifications to different devices, fill out Device field.
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'apprise'">
<div class="mb-3">
<label for="apprise-url" class="form-label">Apprise URL</label>
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
<div class="form-text">
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
<p>
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
</p>
</div>
</div>
<div class="mb-3">
<p>
Status:
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
</p>
</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 v-if="notification.type === 'pushbullet'">
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">Access Token</label>
<input id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" type="text" class="form-control" required>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</p>
</template>
<template v-if="notification.type === 'line'">
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">Channel access token</label>
<input id="line-channel-access-token" v-model="notification.lineChannelAccessToken" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Basic Settings</b>
</div>
<div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label>
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Messaging API</b>
</div>
<div class="form-text" style="margin-top: 8px;">
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
</div>
</template>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button>
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button>
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button>
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
Delete
</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>
</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>
<script>
import { Modal } from 'bootstrap'
import { ucfirst } from "../../server/util";
<script lang="ts">
import { Modal } from "bootstrap"
import { ucfirst } from "../util.ts"
import axios from "axios";
import { useToast } from 'vue-toastification'
import { useToast } from "vue-toastification"
import Confirm from "./Confirm.vue";
const toast = useToast()
export default {
components: {Confirm},
props: {
components: {
Confirm,
},
props: {},
data() {
return {
model: null,
@@ -179,18 +447,43 @@ export default {
notification: {
name: "",
type: null,
gotifyPriority: 8,
},
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() {
this.modal = new Modal(this.$refs.modal)
// TODO: for edit
this.$root.getSocket().emit("getSettings", "notification", (data) => {
// this.notification = data
this.$root.getSocket().emit("checkApprise", (installed) => {
this.appriseInstalled = installed;
})
},
methods: {
@@ -218,6 +511,7 @@ export default {
// Default set to Telegram
this.notification.type = "telegram"
this.notification.gotifyPriority = 8
}
this.modal.show()
@@ -281,35 +575,15 @@ 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>
<style scoped>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<LineChart :chart-data="chartData" :options="chartOptions" />
</template>
<script>
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import "chartjs-adapter-dayjs";
import { LineChart } from "vue-chart-3";
dayjs.extend(utc);
dayjs.extend(timezone);
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
export default {
components: { LineChart },
props: {
monitorId: {
type: Number,
required: true,
},
},
data() {
return {
chartPeriodHrs: 6,
};
},
computed: {
chartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
onResize: (chart) => {
chart.canvas.parentNode.style.position = "relative";
if (screen.width < 576) {
chart.canvas.parentNode.style.height = "275px";
} else if (screen.width < 768) {
chart.canvas.parentNode.style.height = "320px";
} else if (screen.width < 992) {
chart.canvas.parentNode.style.height = "300px";
} else {
chart.canvas.parentNode.style.height = "250px";
}
},
layout: {
padding: {
left: 10,
right: 30,
top: 30,
bottom: 10,
},
},
elements: {
point: {
radius: 0,
},
bar: {
barThickness: "flex",
}
},
scales: {
x: {
type: "time",
time: {
minUnit: "minute",
round: "second",
tooltipFormat: "YYYY-MM-DD HH:mm:ss",
displayFormats: {
minute: "HH:mm",
hour: "MM-DD HH:mm",
}
},
ticks: {
maxRotation: 0,
autoSkipPadding: 30,
},
bounds: "ticks",
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
},
},
y: {
title: {
display: true,
text: "Resp. Time (ms)",
},
offset: false,
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
},
},
y1: {
display: false,
position: "right",
grid: {
drawOnChartArea: false,
},
min: 0,
max: 1,
offset: false,
},
},
bounds: "ticks",
plugins: {
tooltip: {
mode: "nearest",
intersect: false,
padding: 10,
filter: function (tooltipItem) {
return tooltipItem.datasetIndex === 0;
},
callbacks: {
label: (context) => {
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
},
}
},
legend: {
display: false,
},
},
}
},
chartData() {
let ping_data = [];
let down_data = [];
if (this.monitorId in this.$root.heartbeatList) {
ping_data = this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
return {
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
y: beat.ping,
};
});
down_data = this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
return {
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
y: beat.status === 0 ? 1 : 0,
};
});
}
return {
datasets: [
{
data: ping_data,
fill: "origin",
tension: 0.2,
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
},
{
type: "bar",
data: down_data,
borderColor: "#00000000",
backgroundColor: "#DC354568",
yAxisID: "y1",
},
],
};
},
},
};
</script>

View File

@@ -5,35 +5,47 @@
<script>
export default {
props: {
status: Number
status: Number,
},
computed: {
color() {
if (this.status === 0) {
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() {
if (this.status === 0) {
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>
<style scoped>
span {
width: 45px;
width: 64px;
}
</style>

View File

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

View File

@@ -1,83 +1,127 @@
<template>
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
<div class="container-fluid">
Lost connection to the socket server. Reconnecting...
<div :class="classes">
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid">
{{ $root.connectionErrorMsg }}
</div>
</div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
<ul class="nav nav-pills">
<li class="nav-item">
<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>
</header>
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
<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" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<main>
<!-- Add :key to disable vue router re-use the same component -->
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
<footer>
<div class="container-fluid">
Uptime Kuma -
Version: {{ $root.info.version }} -
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
</div>
</footer>
<!-- Mobile Only -->
<div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
<nav v-if="$root.isMobile" class="bottom-nav">
<router-link to="/dashboard" class="nav-link">
<div><font-awesome-icon icon="tachometer-alt" /></div>
Dashboard
</router-link>
<router-link to="/list" class="nav-link">
<div><font-awesome-icon icon="list" /></div>
List
</router-link>
<router-link to="/add" class="nav-link">
<div><font-awesome-icon icon="plus" /></div>
Add
</router-link>
<router-link to="/settings" class="nav-link">
<div><font-awesome-icon icon="cog" /></div>
Settings
</router-link>
</nav>
</div>
<!-- Desktop header -->
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile">
<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"></object>
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
<ul class="nav nav-pills" >
<li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li>
<li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li>
</ul>
</header>
<!-- Mobile header -->
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else>
<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>
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<main>
<!-- Add :key to disable vue router re-use the same component -->
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
<!-- Mobile Only -->
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
<nav class="bottom-nav" v-if="$root.isMobile">
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link>
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div></div>Add</router-link>
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link>
</nav>
</template>
<script>
import Login from "../components/Login.vue";
export default {
components: {
Login
},
data() {
return {
}
components: {
Login,
},
data() {
return {}
},
computed: {
// Theme or Mobile
classes() {
const classes = {};
classes[this.$root.theme] = true;
classes["mobile"] = this.$root.isMobile;
return classes;
}
},
watch: {
$route(to, from) {
this.init();
},
},
mounted() {
this.init();
},
watch: {
$route (to, from) {
this.init();
}
},
methods: {
init() {
if (this.$route.name === "root") {
this.$router.push("/dashboard")
}
},
},
}
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars.scss";
.bottom-nav {
@@ -91,7 +135,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);
text-align: center;
white-space: nowrap;
padding: 0 35px;
padding: 0 10px;
a {
text-align: center;
@@ -115,6 +159,10 @@ export default {
}
}
main {
min-height: calc(100vh - 160px)
}
.title {
font-weight: bold;
}
@@ -129,7 +177,28 @@ export default {
color: white;
}
main {
margin-bottom: 30px;
footer {
color: #AAA;
font-size: 13px;
margin-top: 10px;
padding-bottom: 30px;
margin-left: 10px;
text-align: center;
}
.dark {
header {
background-color: #161B22;
border-bottom-color: #161B22 !important;
span {
color: #F0F6FC;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style>

View File

@@ -1,58 +1,68 @@
import {createApp, h} from "vue";
import {createRouter, createWebHistory} from 'vue-router'
import App from './App.vue'
import Layout from './layouts/Layout.vue'
import EmptyLayout from './layouts/EmptyLayout.vue'
import Settings from "./pages/Settings.vue";
import "bootstrap";
import { createApp, h } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
import App from "./App.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 theme from "./mixins/theme";
import mobile from "./mixins/mobile";
import datetime from "./mixins/datetime";
import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue";
import socket from "./mixins/socket"
import "./assets/app.scss"
import EditMonitor from "./pages/EditMonitor.vue";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
import "bootstrap"
import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue";
import List from "./pages/List.vue";
import { appName } from "./util.ts";
const routes = [
{
path: '/',
path: "/",
component: Layout,
children: [
{
name: "root",
path: '',
path: "",
component: Dashboard,
children: [
{
name: "DashboardHome",
path: '/dashboard',
path: "/dashboard",
component: DashboardHome,
children: [
{
path: '/dashboard/:id',
path: "/dashboard/:id",
component: EmptyLayout,
children: [
{
path: '',
path: "",
component: Details,
},
{
path: '/edit/:id',
path: "/edit/:id",
component: EditMonitor,
},
]
],
},
{
path: '/add',
path: "/add",
component: EditMonitor,
},
]
{
path: "/list",
component: List,
},
],
},
{
path: '/settings',
path: "/settings",
component: Settings,
},
],
@@ -62,13 +72,13 @@ const routes = [
},
{
path: '/setup',
path: "/setup",
component: Setup,
},
]
const router = createRouter({
linkActiveClass: 'active',
linkActiveClass: "active",
history: createWebHistory(),
routes,
})
@@ -76,17 +86,26 @@ const router = createRouter({
const app = createApp({
mixins: [
socket,
theme,
mobile,
datetime
],
render: ()=>h(App)
data() {
return {
appName: appName
}
},
render: () => h(App),
})
app.use(router)
const options = {
position: "bottom-right"
position: "bottom-right",
};
app.use(Toast, options);
app.mount('#app')
app.component("FontAwesomeIcon", FontAwesomeIcon)
app.mount("#app")

57
src/mixins/datetime.js Normal file
View File

@@ -0,0 +1,57 @@
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
/**
* DateTime Mixin
* Handled timezone and format
*/
export default {
data() {
return {
userTimezone: localStorage.timezone || "auto",
}
},
methods: {
datetime(value) {
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
},
date(value) {
return this.datetimeFormat(value, "YYYY-MM-DD");
},
time(value, second = true) {
let secondString;
if (second) {
secondString = ":ss";
} else {
secondString = "";
}
return this.datetimeFormat(value, "HH:mm" + secondString);
},
datetimeFormat(value, format) {
if (value !== undefined && value !== "") {
return dayjs.utc(value).tz(this.timezone).format(format);
}
return "";
}
},
computed: {
timezone() {
if (this.userTimezone === "auto") {
return dayjs.tz.guess()
}
return this.userTimezone
},
}
}

25
src/mixins/mobile.js Normal file
View File

@@ -0,0 +1,25 @@
export default {
data() {
return {
windowWidth: window.innerWidth,
}
},
created() {
window.addEventListener("resize", this.onResize);
},
methods: {
onResize() {
this.windowWidth = window.innerWidth;
},
},
computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
}
}

View File

@@ -1,6 +1,5 @@
import {io} from "socket.io-client";
import { useToast } from 'vue-toastification'
import dayjs from "dayjs";
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
const toast = useToast()
let socket;
@@ -9,6 +8,7 @@ export default {
data() {
return {
info: { },
socket: {
token: null,
firstConnect: true,
@@ -16,7 +16,6 @@ export default {
connectCount: 0,
},
remember: (localStorage.remember !== "0"),
userTimezone: localStorage.timezone || "auto",
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false,
monitorList: { },
@@ -24,39 +23,60 @@ export default {
importantHeartbeatList: { },
avgPingList: { },
uptimeList: { },
certInfoList: {},
notificationList: [],
windowWidth: window.innerWidth,
showListMobile: false,
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
}
},
created() {
window.addEventListener('resize', this.onResize);
window.addEventListener("resize", this.onResize);
let wsHost;
if (localStorage.dev === "dev") {
const env = process.env.NODE_ENV || "production";
if (env === "development" || localStorage.dev === "dev") {
wsHost = ":3001"
} else {
wsHost = ""
}
socket = io(wsHost, {
transports: ['websocket']
transports: ["websocket"],
});
socket.on('setup', (monitorID, data) => {
socket.on("info", (info) => {
this.info = info;
});
socket.on("setup", (monitorID, data) => {
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;
});
socket.on('notificationList', (data) => {
socket.on("notificationList", (data) => {
this.notificationList = data;
});
socket.on('heartbeat', (data) => {
socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];
}
@@ -79,7 +99,6 @@ export default {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
}
if (! (data.monitorID in this.importantHeartbeatList)) {
this.importantHeartbeatList[data.monitorID] = [];
}
@@ -88,7 +107,7 @@ export default {
}
});
socket.on('heartbeatList', (monitorID, data) => {
socket.on("heartbeatList", (monitorID, data) => {
if (! (monitorID in this.heartbeatList)) {
this.heartbeatList[monitorID] = data;
} else {
@@ -96,15 +115,19 @@ export default {
}
});
socket.on('avgPing', (monitorID, data) => {
socket.on("avgPing", (monitorID, data) => {
this.avgPingList[monitorID] = data
});
socket.on('uptime', (monitorID, type, data) => {
socket.on("uptime", (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)) {
this.importantHeartbeatList[monitorID] = data;
} else {
@@ -112,12 +135,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")
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
this.socket.connected = false;
});
socket.on('connect', () => {
socket.on("connect", () => {
console.log("connect")
this.socket.connectCount++;
this.socket.connected = true;
@@ -127,8 +158,22 @@ export default {
this.clearData()
}
if (this.storage().token) {
this.loginByToken(this.storage().token)
let token = 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 {
this.allowLoginDialog = true;
}
@@ -140,20 +185,12 @@ export default {
methods: {
cancelActiveList() {
this.$root.showListMobile = false;
},
onResize() {
this.windowWidth = window.innerWidth;
},
storage() {
return (this.remember) ? localStorage : sessionStorage;
},
getSocket() {
return socket;
return socket;
},
toastRes(res) {
@@ -176,7 +213,7 @@ export default {
this.loggedIn = true;
// Trigger Chrome Save Password
history.pushState({}, '')
history.pushState({}, "")
}
callback(res)
@@ -221,20 +258,6 @@ export default {
computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
timezone() {
if (this.userTimezone === "auto") {
return dayjs.tz.guess()
} else {
return this.userTimezone
}
},
lastHeartbeatList() {
let result = {}
@@ -251,7 +274,7 @@ export default {
let unknown = {
text: "Unknown",
color: "secondary"
color: "secondary",
}
for (let monitorID in this.lastHeartbeatList) {
@@ -262,12 +285,17 @@ export default {
} else if (lastHeartBeat.status === 1) {
result[monitorID] = {
text: "Up",
color: "primary"
color: "primary",
};
} else if (lastHeartBeat.status === 0) {
result[monitorID] = {
text: "Down",
color: "danger"
color: "danger",
};
} else if (lastHeartBeat.status === 2) {
result[monitorID] = {
text: "Pending",
color: "warning",
};
} else {
result[monitorID] = unknown;
@@ -275,16 +303,22 @@ export default {
}
return result;
}
},
},
watch: {
// Reload the SPA if the server version is changed.
"info.version"(to, from) {
if (from && from !== to) {
window.location.reload()
}
},
remember() {
localStorage.remember = (this.remember) ? "1" : "0"
}
},
}
},
}

66
src/mixins/theme.js Normal file
View File

@@ -0,0 +1,66 @@
export default {
data() {
return {
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
userTheme: localStorage.theme,
userHeartbeatBar: localStorage.heartbeatBarTheme,
};
},
mounted() {
// Default Light
if (! this.userTheme) {
this.userTheme = "light";
}
// Default Heartbeat Bar
if (!this.userHeartbeatBar) {
this.userHeartbeatBar = "normal";
}
document.body.classList.add(this.theme);
this.updateThemeColorMeta();
},
computed: {
theme() {
if (this.userTheme === "auto") {
return this.system;
}
return this.userTheme;
}
},
watch: {
userTheme(to, from) {
localStorage.theme = to;
},
theme(to, from) {
document.body.classList.remove(from);
document.body.classList.add(this.theme);
this.updateThemeColorMeta();
},
userHeartbeatBar(to, from) {
localStorage.heartbeatBarTheme = to;
},
heartbeatBarTheme(to, from) {
document.body.classList.remove(from);
document.body.classList.add(this.heartbeatBarTheme);
}
},
methods: {
updateThemeColorMeta() {
if (this.theme === "dark") {
document.querySelector("#theme-color").setAttribute("content", "#161B22");
} else {
document.querySelector("#theme-color").setAttribute("content", "#5cdd8b");
}
}
}
}

View File

@@ -1,150 +1,37 @@
<template>
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile">
<router-link to="/add" class="btn btn-primary">Add New Monitor</router-link>
</div>
<div class="shadow-box list mb-4" v-if="showList">
<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>.
</div>
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in sortedMonitorList" @click="$root.cancelActiveList">
<div class="row">
<div class="col-6 col-md-8 small-padding">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
</div>
<div class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
<div>
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
</div>
<MonitorList />
</div>
<div class="col-12 col-md-7 col-xl-8">
<router-view />
</div>
</div>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
import MonitorList from "../components/MonitorList.vue";
export default {
components: {
Uptime,
HeartbeatBar
MonitorList,
},
data() {
return {
}
return {}
},
computed: {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
})
return result;
},
showList() {
return ! this.$root.isMobile || this.$root.showListMobile;
},
},
methods: {
monitorURL(id) {
return "/dashboard/" + id;
}
}
}
</script>
<style scoped lang="scss">
@import "../assets/vars.scss";
<style lang="scss" scoped>
.container-fluid {
width: 98%
}
.list {
margin-top: 25px;
height: auto;
min-height: calc(100vh - 200px);
.item {
display: block;
text-decoration: none;
padding: 15px 15px 12px 15px;
border-radius: 10px;
transition: all ease-in-out 0.15s;
&.disabled {
opacity: 0.3;
}
.info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
}
}
.badge {
min-width: 58px;
}
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
</style>

View File

@@ -1,75 +1,100 @@
<template>
<transition name="slide-fade" appear>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">
Quick Stats
</h1>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">Quick Stats</h1>
<div class="shadow-box big-padding text-center">
<div class="row">
<div class="col">
<h3>Up</h3>
<span class="num">{{ stats.up }}</span>
<div class="shadow-box big-padding text-center">
<div class="row">
<div class="col">
<h3>Up</h3>
<span class="num">{{ stats.up }}</span>
</div>
<div class="col">
<h3>Down</h3>
<span class="num text-danger">{{ stats.down }}</span>
</div>
<div class="col">
<h3>Unknown</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
</div>
<div class="col">
<h3>Pause</h3>
<span class="num text-secondary">{{ stats.pause }}</span>
</div>
</div>
<div class="col">
<h3>Down</h3>
<span class="num text-danger">{{ stats.down }}</span>
</div>
<div class="col">
<h3>Unknown</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
</div>
<div class="col">
<h3>Pause</h3>
<span class="num text-secondary">{{ stats.pause }}</span>
<div v-if="false" class="row">
<div class="col-3">
<h3>Uptime</h3>
<p>(24-hour)</p>
<span class="num" />
</div>
<div class="col-3">
<h3>Uptime</h3>
<p>(30-day)</p>
<span class="num" />
</div>
</div>
</div>
<div class="row" v-if="false">
<div class="col-3">
<h3>Uptime</h3>
<p>(24-hour)</p>
<span class="num"></span>
</div>
<div class="col-3">
<h3>Uptime</h3>
<p>(30-day)</p>
<span class="num"></span>
<div class="shadow-box" style="margin-top: 25px;overflow-x: scroll">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index">
<td>{{ beat.name }}</td>
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="4">
No important events
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div>
</div>
<div class="shadow-box" style="margin-top: 25px;">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="beat in importantHeartBeatList">
<td>{{ beat.name }}</td>
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="4">No important events</td>
</tr>
</tbody>
</table>
</div>
</div>
</transition>
<router-view ref="child" />
</template>
<script>
import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue";
import Pagination from "v-pagination-3";
export default {
components: {Datetime, Status},
components: {
Datetime,
Status,
Pagination,
},
data() {
return {
page: 1,
perPage: 25,
heartBeatList: [],
}
},
computed: {
stats() {
let result = {
@@ -90,6 +115,8 @@ export default {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else if (beat.status === 2) {
result.up++;
} else {
result.unknown++;
}
@@ -105,7 +132,7 @@ export default {
let result = [];
for (let monitorID in this.$root.importantHeartbeatList) {
let list = this.$root.importantHeartbeatList[monitorID]
let list = this.$root.importantHeartbeatList[monitorID]
result = result.concat(list);
}
@@ -120,20 +147,30 @@ export default {
result.sort((a, b) => {
if (a.time > b.time) {
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;
}
}
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars";
.num {

View File

@@ -1,93 +1,180 @@
<template>
<h1> {{ monitor.name }}</h1>
<p class="url">
<a :href="monitor.url" target="_blank" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">{{ monitor.url }}</a>
<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 === 'keyword'">
<br />
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
</span>
</p>
<transition name="slide-fade" appear>
<div>
<h1> {{ monitor.name }}</h1>
<p class="url">
<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 === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
<span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
</span>
</p>
<div class="functions">
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
<button class="btn btn-danger" @click="deleteDialog">Delete</button>
</div>
<div class="functions">
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<font-awesome-icon icon="pause" /> Pause
</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 class="shadow-box">
<div class="row">
<div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">Check every {{ monitor.interval }} seconds.</span>
<div class="shadow-box">
<div class="row">
<div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">Check every {{ monitor.interval }} seconds.</span>
</div>
<div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
</div>
</div>
</div>
<div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div class="col">
<h4>{{ pingTitle }}</h4>
<p>(Current)</p>
<span class="num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<CountUp :value="ping" />
</a>
</span>
</div>
<div class="col">
<h4>Avg. {{ pingTitle }}</h4>
<p>(24-hour)</p>
<span class="num"><CountUp :value="avgPing" /></span>
</div>
<div class="col">
<h4>Uptime</h4>
<p>(24-hour)</p>
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
</div>
<div class="col">
<h4>Uptime</h4>
<p>(30-day)</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
</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>
<transition name="slide-fade" appear>
<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>
</transition>
<div v-if="showPingChartBox" class="shadow-box big-padding text-center ping-chart-wrapper">
<div class="row">
<div class="col">
<PingChart :monitor-id="monitor.id" />
</div>
</div>
</div>
<div class="shadow-box">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index">
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">
No important events
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div>
<Confirm ref="confirmPause" @yes="pauseMonitor">
Are you sure want to pause?
</Confirm>
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
Are you sure want to delete this monitor?
</Confirm>
</div>
</div>
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div class="col">
<h4>{{ pingTitle }}</h4>
<p>(Current)</p>
<span class="num"><CountUp :value="ping" /></span>
</div>
<div class="col">
<h4>Avg.{{ pingTitle }}</h4>
<p>(24-hour)</p>
<span class="num"><CountUp :value="avgPing" /></span>
</div>
<div class="col">
<h4>Uptime</h4>
<p>(24-hour)</p>
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
</div>
<div class="col">
<h4>Uptime</h4>
<p>(30-day)</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div>
</div>
</div>
<div class="shadow-box">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="beat in importantHeartBeatList">
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">No important events</td>
</tr>
</tbody>
</table>
</div>
<Confirm ref="confirmPause" @yes="pauseMonitor">
Are you sure want to pause?
</Confirm>
<Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor">
Are you sure want to delete this monitor?
</Confirm>
</transition>
</template>
<script>
import { useToast } from 'vue-toastification'
import { defineAsyncComponent } from "vue";
import { useToast } from "vue-toastification"
const toast = useToast()
import Confirm from "../components/Confirm.vue";
import HeartbeatBar from "../components/HeartbeatBar.vue";
@@ -95,6 +182,8 @@ import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
export default {
components: {
@@ -104,13 +193,16 @@ export default {
HeartbeatBar,
Confirm,
Status,
},
mounted() {
Pagination,
PingChart,
},
data() {
return {
page: 1,
perPage: 25,
heartBeatList: [],
toggleCertInfoBox: false,
showPingChartBox: true,
}
},
computed: {
@@ -118,9 +210,9 @@ export default {
pingTitle() {
if (this.monitor.type === "http") {
return "Response"
} else {
return "Ping"
}
return "Ping"
},
monitor() {
@@ -131,42 +223,65 @@ export default {
lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id]
} else {
return { status: -1 }
}
return {
status: -1,
}
},
ping() {
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
return this.lastHeartBeat.ping;
} else {
return "N/A"
}
return "N/A"
},
avgPing() {
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
return this.$root.avgPingList[this.monitor.id];
} else {
return "N/A"
}
return "N/A"
},
importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) {
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id]
} else {
return [];
}
return [];
},
status() {
if (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: {
@@ -204,15 +319,50 @@ export default {
toast.error(res.msg);
}
})
}
},
}
},
}
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
@media (max-width: 767px) {
.badge {
margin-top: 14px;
}
}
@media (max-width: 550px) {
.functions {
text-align: center;
}
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
}
.ping-chart-wrapper {
padding: 10px !important;
}
}
@media (max-width: 400px) {
.btn {
display: inline-flex;
flex-direction: column;
align-items: center;
padding-top: 10px;
}
a.btn {
padding-left: 25px;
padding-right: 25px;
}
}
.url {
color: $primary;
margin-bottom: 20px;
@@ -251,4 +401,22 @@ table {
font-size: 13px;
color: #AAA;
}
.stats {
padding: 10px;
.col {
margin: 20px 0;
}
}
.keyword {
color: black;
}
.dark {
.keyword {
color: $dark-font-color;
}
}
</style>

View File

@@ -1,104 +1,178 @@
<template>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">General</h2>
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2>General</h2>
<div class="my-3">
<label for="type" class="form-label">Monitor Type</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - Keyword
</option>
</select>
</div>
<div class="mb-3">
<label for="type" class="form-label">Monitor Type</label>
<select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type">
<option value="http">HTTP(s)</option>
<option value="port">TCP Port</option>
<option value="ping">Ping</option>
<option value="keyword">HTTP(s) - Keyword</option>
</select>
<div class="my-3">
<label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">URL</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
<div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">Keyword</label>
<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>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'port' " class="my-3">
<label for="port" class="form-label">Port</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="my-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div>
<div class="my-3">
<label for="maxRetries" class="form-label">Retries</label>
<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 class="mt-5 mb-2">Advanced</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites
</label>
</div>
<div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down">
Upside Down Mode
</label>
<div class="form-text">
Flip the status upside down. If the service is reachable, it is DOWN.
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="maxRedirects" class="form-label">Max. Redirects</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
Maximum number of redirects to follow. Set to 0 to disable redirects.
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Pick Accepted Status Codes..."
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<div class="form-text">
Select status codes which are considered as a successful response.
</div>
</div>
<div class="mt-5 mb-1">
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
</div>
</div>
<div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2 class="mb-2">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 my-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 class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" v-model="monitor.name" required>
</div>
<div class="mb-3" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
<label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required>
</div>
<div class="mb-3" v-if="monitor.type === 'keyword' ">
<label for="keyword" class="form-label">Keyword</label>
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required>
<div class="form-text">Search keyword in plain html or JSON response and it is case-sensitive</div>
</div>
<div class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' ">
<label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" v-model="monitor.hostname" required>
</div>
<div class="mb-3" v-if="monitor.type === 'port' ">
<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">
</div>
<div class="mb-3">
<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">
</div>
<div>
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
</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>
</form>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
</div>
<NotificationDialog ref="notificationDialog" />
</div>
</div>
</form>
<NotificationDialog ref="notificationDialog" />
</transition>
</template>
<script>
import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from 'vue-toastification'
import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
const toast = useToast()
export default {
components: {
NotificationDialog
},
mounted() {
this.init();
NotificationDialog,
VueMultiselect,
},
data() {
return {
processing: false,
monitor: {
notificationIDList: {},
},
acceptedStatusCodeOptions: [],
}
},
computed: {
pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit"
@@ -108,7 +182,29 @@ export default {
},
isEdit() {
return this.$route.path.startsWith("/edit");
},
},
watch: {
"$route.fullPath"() {
this.init();
},
},
mounted() {
this.init();
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
},
methods: {
init() {
@@ -119,7 +215,12 @@ export default {
name: "",
url: "https://",
interval: 60,
maxretries: 0,
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
}
} else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@@ -154,16 +255,45 @@ export default {
this.$root.toastRes(res)
})
}
}
},
watch: {
'$route.fullPath' () {
this.init();
}
},
},
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
@import "../assets/vars.scss";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: 50rem;
background: $primary !important;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
}
</style>
<style scoped>
.shadow-box {
padding: 20px;

14
src/pages/List.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<MonitorList />
</template>
<script>
import MonitorList from "../components/MonitorList.vue";
export default {
components: {
MonitorList,
},
}
</script>

View File

@@ -1,118 +1,211 @@
<template>
<h1 class="mb-3">Settings</h1>
<transition name="slide-fade" appear>
<div>
<h1 v-show="show" class="mb-3">
Settings
</h1>
<div class="shadow-box">
<div class="row">
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">General</h2>
<div class="col-md-6">
<h2>General</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3">
<label for="timezone" class="form-label">Timezone</label>
<select class="form-select" id="timezone" v-model="$root.userTimezone">
<option value="auto">Auto: {{ guessTimezone }}</option>
<option v-for="timezone in timezoneList" :value="timezone.value">{{ timezone.name }}</option>
</select>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3">
<label for="timezone" class="form-label">Theme</label>
<div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
<label class="btn btn-outline-primary" for="btncheck1">Light</label>
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
<label class="btn btn-outline-primary" for="btncheck2">Dark</label>
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
<label class="btn btn-outline-primary" for="btncheck3">Auto</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Theme - Heartbeat Bar</label>
<div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
<label class="btn btn-outline-primary" for="btncheck4">Normal</label>
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
<label class="btn btn-outline-primary" for="btncheck5">Bottom</label>
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
<label class="btn btn-outline-primary" for="btncheck6">None</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="timezone" class="form-label">Timezone</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">
Auto: {{ guessTimezone }}
</option>
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Search Engine Visibility</label>
<div class="form-check">
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
<label class="form-check-label" for="searchEngineIndexYes">
Allow indexing
</label>
</div>
<div class="form-check">
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
<label class="form-check-label" for="searchEngineIndexNo">
Discourage search engines from indexing site
</label>
</div>
</div>
<div>
<button class="btn btn-primary" type="submit">
Save
</button>
</div>
</form>
<template v-if="loaded">
<template v-if="! settings.disableAuth">
<h2 class="mt-5 mb-2">Change Password</h2>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<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 class="mt-5 mb-2">Advanced</h2>
<div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
</div>
</template>
</div>
<div>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
<div class="notification-list col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>Change Password</h2>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<label for="current-password" class="form-label">Current Password</label>
<input type="password" class="form-control" id="current-password" required v-model="password.currentPassword">
</div>
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<p v-else>
Please assign a notification to monitor(s) to get it to work.
</p>
<div class="mb-3">
<label for="new-password" class="form-label">New Password</label>
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword">
</div>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</li>
</ul>
<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>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</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 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>
<p v-else>Please assign the notification to monitor(s) to get it works.</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li class="list-group-item" v-for="notification in $root.notificationList">
{{ notification.name }}<br />
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</li>
</ul>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
</div>
<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>
</div>
</div>
<NotificationDialog ref="notificationDialog" />
</transition>
</template>
<script>
import Confirm from "../components/Confirm.vue";
import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import utc from "dayjs/plugin/utc"
import timezone from "dayjs/plugin/timezone"
import NotificationDialog from "../components/NotificationDialog.vue";
dayjs.extend(utc)
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()
export default {
components: {
NotificationDialog
NotificationDialog,
Confirm,
},
data() {
return {
timezoneList: timezoneList(),
guessTimezone: dayjs.tz.guess(),
show: true,
invalidPassword: false,
password: {
currentPassword: "",
newPassword: "",
repeatNewPassword: "",
}
},
settings: {
},
loaded: false,
}
},
watch: {
"password.repeatNewPassword"() {
this.invalidPassword = false;
},
},
mounted() {
this.loadSettings();
},
methods: {
saveGeneral() {
localStorage.timezone = this.$root.userTimezone;
toast.success("Saved.")
this.saveSettings();
},
savePassword() {
@@ -129,17 +222,68 @@ export default {
})
}
},
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
if (this.settings.searchEngineIndex === undefined) {
this.settings.searchEngineIndex = false;
}
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>
<style scoped>
.shadow-box {
padding: 20px;
<style lang="scss" scoped>
@import "../assets/vars.scss";
.shadow-box {
padding: 20px;
}
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #fff;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #000;
}
}
</style>

View File

@@ -2,38 +2,42 @@
<div class="form-container">
<div class="form">
<form @submit.prevent="submit">
<div>
<object width="64" height="64" data="/icon.svg"></object>
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">Uptime Kuma</div>
<object width="64" height="64" data="/icon.svg" />
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
Uptime Kuma
</div>
</div>
<p class="mt-3">Create your admin account</p>
<p class="mt-3">
Create your admin account
</p>
<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>
</div>
<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>
</div>
<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>
</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>
</div>
</div>
</template>
<script>
import { useToast } from 'vue-toastification'
import { useToast } from "vue-toastification"
const toast = useToast()
export default {
@@ -70,8 +74,8 @@ export default {
this.$router.push("/")
}
})
}
}
},
},
}
</script>

View File

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

70
src/util.js Normal file
View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const _dayjs = require("dayjs");
const dayjs = _dayjs;
exports.isDev = process.env.NODE_ENV === "development";
exports.appName = "Uptime Kuma";
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 (exports.isDev) {
console.log(msg);
}
}
exports.debug = debug;
function polyfill() {
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
return this.replace(new RegExp(str, "g"), newStr);
};
}
}
exports.polyfill = polyfill;
class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
print(name) {
if (exports.isDev) {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
}
}
}
exports.TimeLogger = TimeLogger;
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
exports.getRandomArbitrary = getRandomArbitrary;
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
exports.getRandomInt = getRandomInt;

105
src/util.ts Normal file
View File

@@ -0,0 +1,105 @@
// @ts-nocheck
// 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.
import * as _dayjs from "dayjs";
const dayjs = _dayjs;
export const isDev = process.env.NODE_ENV === "development";
export const appName = "Uptime Kuma";
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 (isDev) {
console.log(msg);
}
}
export function polyfill() {
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, "g"), newStr);
};
}
}
export class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
print(name) {
if (isDev) {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
}
}
}
/**
* Returns a random number between min (inclusive) and max (exclusive)
*/
export function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
/**
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
*
* Returns a random integer between min (inclusive) and max (inclusive).
* The value is no lower than min (or the next integer greater than min
* if min isn't an integer) and no greater than max (or the next integer
* lower than max if max isn't an integer).
* Using Math.round() will give you a non-uniform distribution!
*/
export function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@@ -0,0 +1,4 @@
FROM alpine:3
RUN apk add --update nodejs npm git
COPY ./install.sh .
RUN /bin/sh install.sh local /opt/uptime-kuma 3000 0.0.0.0

View File

@@ -0,0 +1,4 @@
FROM centos:7
COPY ./install.sh .
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0

View File

@@ -0,0 +1,4 @@
FROM centos:8
COPY ./install.sh .
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0

View File

@@ -0,0 +1,10 @@
FROM debian
# Test invalid node version, these commands install nodejs 10
# RUN apt-get update
# RUN apt --yes install nodejs
# RUN ln -s /usr/bin/nodejs /usr/bin/node
# RUN node -v
COPY ./install.sh .
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0

View File

@@ -0,0 +1,10 @@
FROM ubuntu
# Test invalid node version, these commands install nodejs 10
# RUN apt-get update
# RUN apt --yes install nodejs
# RUN ln -s /usr/bin/nodejs /usr/bin/node
# RUN node -v
COPY ./install.sh .
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0

View File

@@ -0,0 +1,10 @@
FROM ubuntu:16.04
# Test invalid node version, these commands install nodejs 10
RUN apt-get update
RUN apt --yes install nodejs
# RUN ln -s /usr/bin/nodejs /usr/bin/node
# RUN node -v
COPY ./install.sh .
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0

17
tsconfig.json Normal file
View File

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

View File

@@ -1,14 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import legacy from "@vitejs/plugin-legacy"
import vue from "@vitejs/plugin-vue"
import { defineConfig } from "vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
]
plugins: [
vue(),
legacy({
targets: ["ie > 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
})
]
})