Compare commits

...

201 Commits
1.0.8 ... 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
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
66 changed files with 19196 additions and 8123 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

@@ -18,6 +18,8 @@ README.md
package-lock.json package-lock.json
yarn.lock yarn.lock
app.json app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
### .gitignore content (commented rules are duplicated) ### .gitignore content (commented rules are duplicated)

View File

@@ -36,6 +36,11 @@ module.exports = {
"no-multi-spaces": ["error", { "no-multi-spaces": ["error", {
ignoreEOLComments: true, ignoreEOLComments: true,
}], }],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"curly": "error", "curly": "error",
"object-curly-spacing": ["error", "always"], "object-curly-spacing": ["error", "always"],
"object-curly-newline": "off", "object-curly-newline": "off",
@@ -62,9 +67,6 @@ module.exports = {
exceptAfterSingleLine: true, exceptAfterSingleLine: true,
}], }],
"no-unneeded-ternary": "error", "no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"], "array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
//'prefer-template': 'error', //'prefer-template': 'error',

View File

@@ -6,5 +6,12 @@ labels: help
assignees: '' 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:

View File

@@ -7,6 +7,9 @@ assignees: ''
--- ---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@@ -20,15 +23,16 @@ Steps to reproduce the behavior:
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Info**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Error Log**
- Uptime Kuma Version: It is easier for us to find out the problem.
- Using Docker?: Yes/No
- OS:
- Browser:
**Additional context**
Add any other context about the problem here.

View File

@@ -6,6 +6,8 @@ labels: enhancement
assignees: '' 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.** **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 [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 5 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

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

View File

@@ -2,7 +2,6 @@
<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> <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%"> <div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" /> <img src="./public/icon.svg" width="128" alt="" />
</div> </div>
@@ -11,100 +10,57 @@ It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" /> <img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
# Features ## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping. * Monitoring uptime for HTTP(s) / TCP / Ping.
* Fancy, Reactive, Fast UI/UX. * Fancy, Reactive, Fast UI/UX.
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. * Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
* 20 seconds interval. * 20 seconds interval.
# How to Use ## 🔧 How to Install
## Docker ### 🚀 Installer via cli
Interactive cli installer, supports Docker or without Docker.
```bash ```bash
# Create a volume curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
docker volume create uptime-kuma ```
# Start the container ### 🐳 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 docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
``` ```
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after started.
Change Port and Volume ### Advanced Installation
```bash If you need more options or need to browse via a reserve proxy, please read:
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
## Without Docker https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
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
# Listen to different port or hostname
pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0.0.0
```
Browse to http://localhost:3001 after started.
## (Optional) One more step for Reverse Proxy
This is optional for someone who want to do reverse proxy. ## 🆙 How to Update
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket. Please read:
Please read wiki for more info: https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
## One-click Deploy ## 🆕 What's Next?
<!---
Abort. Heroku instance killed the server.js if idle, stupid.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.8)
-->
[![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)
# How to Update
### Docker
Re-pull the latest docker image and create another container with the same volume.
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
### Without Docker
```bash
git fetch --all
git checkout 1.0.8 --force
npm install
npm run build
pm2 restart uptime-kuma
```
# What's Next?
I will mark requests/issues to the next milestone. I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones https://github.com/louislam/uptime-kuma/milestones
# More Screenshots ## 🖼 More Screenshots
Dark Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
Settings Page: Settings Page:
@@ -114,8 +70,7 @@ Telegram Notification Sample:
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" /> <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. * Want to build a fancy UI.
@@ -124,11 +79,9 @@ Telegram Notification Sample:
* Try to use WebSocket with SPA instead of REST API. * Try to use WebSocket with SPA instead of REST API.
* Deploy my first Docker image to Docker Hub. * Deploy my first Docker image to Docker Hub.
If you love this project, please consider giving me a ⭐. If you love this project, please consider giving me a ⭐.
## Contribute
# Contribute
If you want to report a bug or request a new feature. Free feel to open a new issue. If you want to report a bug or request a new feature. Free feel to open a new issue.

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

View File

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

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;

View File

@@ -5,8 +5,9 @@ WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt # split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
ln -s /usr/bin/python3 /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python && \
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \ npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \
apk del .build-deps apk del .build-deps && \
rm -f /usr/bin/python
# Touching above code may causes sqlite3 re-compile again, painful slow. # Touching above code may causes sqlite3 re-compile again, painful slow.
@@ -14,14 +15,9 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib 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 && \ RUN pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache rm -rf /root/.cache
RUN apprise --version
# New things add here
COPY . . COPY . .
RUN npm install && \ RUN npm install && npm run build && npm prune
npm run build && \
npm prune
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]

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

View File

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

View File

@@ -1,25 +1,9 @@
/** const pkg = require("../package.json");
* 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);
};
}
const pkg = require('../package.json');
const fs = require("fs"); const fs = require("fs");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version const oldVersion = pkg.version
const newVersion = oldVersion + "-nightly" const newVersion = oldVersion + "-nightly"
@@ -35,6 +19,6 @@ if (newVersion) {
// Process README.md // Process README.md
if (fs.existsSync("README.md")) { if (fs.existsSync("README.md")) {
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) 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,39 +0,0 @@
/**
* 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);
};
}
const pkg = require('../package.json');
const fs = require("fs");
const oldVersion = pkg.version
const newVersion = process.argv[2]
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
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
}

View File

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

23102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.0.8", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -13,58 +13,70 @@
"dev": "vite --host", "dev": "vite --host",
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-demo-server": "set NODE_ENV=demo && node server/server.js",
"update": "", "update": "",
"build": "vite build", "build": "vite build",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.8 --target release . --push", "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.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": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.8 && npm install && npm run build", "setup": "git checkout 1.3.0 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js", "update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.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": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4", "@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.2", "@louislam/sqlite3": "^5.0.3",
"@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0", "args-parser": "^1.3.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bootstrap": "^5.0.2", "bootstrap": "^5.1.0",
"chart.js": "^3.5.0",
"chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2", "http-graceful-shutdown": "^3.1.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"password-hash": "^1.2.2", "password-hash": "^1.2.2",
"prom-client": "^13.1.0", "prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0", "prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.20", "redbean-node": "0.0.21",
"socket.io": "^4.1.3", "socket.io": "^4.1.3",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6", "v-pagination-3": "^0.1.6",
"vue": "^3.0.5", "vue": "^3.2.2",
"vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2", "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" "vue-toastification": "^2.0.0-rc.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.10", "@babel/eslint-parser": "^7.15.0",
"@types/bootstrap": "^5.0.17", "@types/bootstrap": "^5.1.1",
"@vitejs/plugin-legacy": "^1.5.0", "@vitejs/plugin-legacy": "^1.5.1",
"@vitejs/plugin-vue": "^1.3.0", "@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.1.5", "@vue/compiler-sfc": "^3.2.2",
"core-js": "^3.15.2", "core-js": "^3.16.1",
"eslint": "^7.31.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.14.0", "eslint-plugin-vue": "^7.16.0",
"sass": "^1.36.0", "sass": "^1.37.5",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0", "stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,16 +1,52 @@
const fs = require("fs"); const fs = require("fs");
const { sleep } = require("../src/util"); const { sleep } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { const { setSetting, setting } = require("./util-server");
setSetting, setting, const knex = require("knex");
} = require("./util-server"); const sqlite3 = require("@louislam/sqlite3");
class Database { class Database {
static templatePath = "./db/kuma.db" static templatePath = "./db/kuma.db"
static path = "./data/kuma.db"; static path = "./data/kuma.db";
static latestVersion = 4; static latestVersion = 6;
static noReject = true; 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() { static async patch() {
let version = parseInt(await setting("database_version")); let version = parseInt(await setting("database_version"));
@@ -24,6 +60,8 @@ class Database {
if (version === this.latestVersion) { if (version === this.latestVersion) {
console.info("Database no need to patch"); console.info("Database no need to patch");
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else { } else {
console.info("Database patch is needed") console.info("Database patch is needed")
@@ -31,6 +69,16 @@ class Database {
const backupPath = "./data/kuma.db.bak" + version; const backupPath = "./data/kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath); 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 catch anything here, if gone wrong, restore the backup
try { try {
for (let i = version + 1; i <= this.latestVersion; i++) { for (let i = version + 1; i <= this.latestVersion; i++) {
@@ -93,27 +141,10 @@ class Database {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async close() { static async close() {
const listener = (reason, p) => { if (this.sqliteInstance) {
Database.noReject = false; this.sqliteInstance.close();
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB")
while (true) {
Database.noReject = true;
await R.close()
await sleep(2000)
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db")
}
} }
console.log("SQLite closed") console.log("Stopped database");
process.removeListener("unhandledRejection", listener);
} }
} }

View File

@@ -6,11 +6,12 @@ dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, checkCertificate } = require("../util-server"); const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification") const { Notification } = require("../notification")
const version = require("../../package.json").version;
/** /**
* status: * status:
@@ -45,6 +46,8 @@ class Monitor extends BeanModel {
keyword: this.keyword, keyword: this.keyword,
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
notificationIDList, notificationIDList,
}; };
} }
@@ -65,6 +68,10 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown); return Boolean(this.upsideDown);
} }
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
@@ -73,6 +80,10 @@ class Monitor extends BeanModel {
const beat = async () => { const beat = async () => {
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) { if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id, this.id,
@@ -104,25 +115,32 @@ class Monitor extends BeanModel {
// Use Custom agent to disable session reuse // Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940 // https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, { let res = await axios.get(this.url, {
timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"User-Agent": "Uptime-Kuma", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, maxCachedSessions: 0,
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: ! this.getIgnoreTls(),
}), }),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
}); });
bean.msg = `${res.status} - ${res.statusText}` bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used // Check certificate if https is used
let certInfoStartTime = dayjs().valueOf(); let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") { if (this.getUrl()?.protocol === "https:") {
try { try {
await this.updateTlsInfo(checkCertificate(res)); tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) { } catch (e) {
console.error(e.message) if (e.message !== "No TLS certificate in response") {
console.error(e.message)
}
} }
} }
@@ -240,7 +258,7 @@ class Monitor extends BeanModel {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
} }
prometheus.update(bean) prometheus.update(bean, tlsInfo)
io.to(this.user_id).emit("heartbeat", bean.toJSON()); io.to(this.user_id).emit("heartbeat", bean.toJSON());
@@ -275,7 +293,7 @@ class Monitor extends BeanModel {
/** /**
* Store TLS info to database * Store TLS info to database
* @param checkCertificateResult * @param checkCertificateResult
* @returns {Promise<void>} * @returns {Promise<object>}
*/ */
async updateTlsInfo(checkCertificateResult) { async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@@ -287,13 +305,15 @@ class Monitor extends BeanModel {
} }
tls_info_bean.info_json = JSON.stringify(checkCertificateResult); tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean); await R.store(tls_info_bean);
return checkCertificateResult;
} }
static async sendStats(io, monitorID, userID) { static async sendStats(io, monitorID, userID) {
Monitor.sendAvgPing(24, io, monitorID, userID); await Monitor.sendAvgPing(24, io, monitorID, userID);
Monitor.sendUptime(24, io, monitorID, userID); await Monitor.sendUptime(24, io, monitorID, userID);
Monitor.sendUptime(24 * 30, io, monitorID, userID); await Monitor.sendUptime(24 * 30, io, monitorID, userID);
Monitor.sendCertInfo(io, monitorID, userID); await Monitor.sendCertInfo(io, monitorID, userID);
} }
/** /**
@@ -301,6 +321,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours * @param duration : int Hours
*/ */
static async sendAvgPing(duration, io, monitorID, userID) { static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let avgPing = parseInt(await R.getCell(` let avgPing = parseInt(await R.getCell(`
SELECT AVG(ping) SELECT AVG(ping)
FROM heartbeat FROM heartbeat
@@ -311,6 +333,8 @@ class Monitor extends BeanModel {
monitorID, monitorID,
])); ]));
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
io.to(userID).emit("avgPing", monitorID, avgPing); io.to(userID).emit("avgPing", monitorID, avgPing);
} }
@@ -330,6 +354,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours * @param duration : int Hours
*/ */
static async sendUptime(duration, io, monitorID, userID) { static async sendUptime(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let sec = duration * 3600; let sec = duration * 3600;
let heartbeatList = await R.getAll(` let heartbeatList = await R.getAll(`
@@ -341,6 +367,8 @@ class Monitor extends BeanModel {
monitorID, monitorID,
]); ]);
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
let downtime = 0; let downtime = 0;
let total = 0; let total = 0;
let uptime; let 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

@@ -84,40 +84,78 @@ class Notification {
} else if (notification.type === "discord") { } else if (notification.type === "discord") {
try { try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing. // If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let discordtestdata = {
username: "Uptime-Kuma", username: discordDisplayName,
content: msg, content: msg,
} }
await axios.post(notification.discordWebhookUrl, data) await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg; return okMsg;
} }
// If heartbeatJSON is not null, we go into the normal alerting loop. // If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == 0) { if (heartbeatJSON["status"] == 0) {
var alertColor = "16711680"; 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) { } else if (heartbeatJSON["status"] == 1) {
var alertColor = "65280"; 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;
} }
let data = {
username: "Uptime-Kuma",
embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Message",
value: msg,
},
],
}],
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
@@ -137,6 +175,52 @@ class Notification {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "pushy") {
try {
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
"message": "Uptime-Kuma"
},
"notification": {
"body": msg,
"badge": 1,
"sound": "ping.aiff"
}
})
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "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") { } else if (notification.type === "slack") {
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
@@ -270,6 +354,88 @@ class Notification {
throwGeneralAxiosError(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 { } else {
throw new Error("Notification type is not supported") throw new Error("Notification type is not supported")
} }
@@ -313,15 +479,21 @@ class Notification {
static async smtp(notification, msg) { static async smtp(notification, msg) {
let transporter = nodemailer.createTransport({ const config = {
host: notification.smtpHost, host: notification.smtpHost,
port: notification.smtpPort, port: notification.smtpPort,
secure: notification.smtpSecure, 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, user: notification.smtpUsername,
pass: notification.smtpPassword, pass: notification.smtpPassword,
}, };
}); }
let transporter = nodemailer.createTransport(config);
// send mail with defined transport object // send mail with defined transport object
await transporter.sendMail({ await transporter.sendMail({

View File

@@ -1,12 +1,14 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js // https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows // Fixed on Windows
const net = require("net");
let spawn = require("child_process").spawn, const spawn = require("child_process").spawn,
events = require("events"), events = require("events"),
fs = require("fs"), fs = require("fs"),
WIN = /^win/.test(process.platform), WIN = /^win/.test(process.platform),
LIN = /^linux/.test(process.platform), LIN = /^linux/.test(process.platform),
MAC = /^darwin/.test(process.platform); MAC = /^darwin/.test(process.platform);
FBSD = /^freebsd/.test(process.platform);
const { debug } = require("../src/util");
module.exports = Ping; module.exports = Ping;
@@ -24,14 +26,42 @@ function Ping(host, options) {
this._bin = "c:/windows/system32/ping.exe"; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} else if (LIN) { } else if (LIN) {
this._bin = "/bin/ping"; this._bin = "/bin/ping";
this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; // need to verify this 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 if (MAC) { } else if (MAC) {
this._bin = "/sbin/ping";
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._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; 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 { } else {
throw new Error("Could not detect your ping binary."); throw new Error("Could not detect your ping binary.");
} }
@@ -49,9 +79,9 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING // SEND A PING
// =========== // ===========
Ping.prototype.send = function(callback) { Ping.prototype.send = function (callback) {
let self = this; let self = this;
callback = callback || function(err, ms) { callback = callback || function (err, ms) {
if (err) { if (err) {
return self.emit("error", err); return self.emit("error", err);
} }
@@ -62,27 +92,27 @@ Ping.prototype.send = function(callback) {
this._ping = spawn(this._bin, this._args); // spawn the binary this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping.on("error", function(err) { // handle binary errors this._ping.on("error", function (err) { // handle binary errors
_errored = true; _errored = true;
callback(err); callback(err);
}); });
this._ping.stdout.on("data", function(data) { // log stdout this._ping.stdout.on("data", function (data) { // log stdout
this._stdout = (this._stdout || "") + data; this._stdout = (this._stdout || "") + data;
}); });
this._ping.stdout.on("end", function() { this._ping.stdout.on("end", function () {
_ended = true; _ended = true;
if (_exited && !_errored) { if (_exited && !_errored) {
onEnd.call(self._ping); onEnd.call(self._ping);
} }
}); });
this._ping.stderr.on("data", function(data) { // log stderr this._ping.stderr.on("data", function (data) { // log stderr
this._stderr = (this._stderr || "") + data; this._stderr = (this._stderr || "") + data;
}); });
this._ping.on("exit", function(code) { // handle complete this._ping.on("exit", function (code) { // handle complete
_exited = true; _exited = true;
if (_ended && !_errored) { if (_ended && !_errored) {
onEnd.call(self._ping); onEnd.call(self._ping);
@@ -105,15 +135,15 @@ Ping.prototype.send = function(callback) {
ms = stdout.match(self._regmatch); // parse out the ##ms response ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms; ms = (ms && ms[1]) ? Number(ms[1]) : ms;
callback(null, ms); callback(null, ms, stdout);
} }
}; };
// CALL Ping#send(callback) ON A TIMER // CALL Ping#send(callback) ON A TIMER
// =================================== // ===================================
Ping.prototype.start = function(callback) { Ping.prototype.start = function (callback) {
let self = this; let self = this;
this._i = setInterval(function() { this._i = setInterval(function () {
self.send(callback); self.send(callback);
}, (self._options.interval || 5000)); }, (self._options.interval || 5000));
self.send(callback); self.send(callback);
@@ -121,6 +151,6 @@ Ping.prototype.start = function(callback) {
// STOP SENDING PINGS // STOP SENDING PINGS
// ================== // ==================
Ping.prototype.stop = function() { Ping.prototype.stop = function () {
clearInterval(this._i); clearInterval(this._i);
}; };

View File

@@ -1,22 +1,33 @@
const PrometheusClient = require('prom-client'); const PrometheusClient = require("prom-client");
const commonLabels = [ const commonLabels = [
'monitor_name', "monitor_name",
'monitor_type', "monitor_type",
'monitor_url', "monitor_url",
'monitor_hostname', "monitor_hostname",
'monitor_port', "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({ const monitor_response_time = new PrometheusClient.Gauge({
name: 'monitor_response_time', name: "monitor_response_time",
help: 'Monitor Response Time (ms)', help: "Monitor Response Time (ms)",
labelNames: commonLabels labelNames: commonLabels
}); });
const monitor_status = new PrometheusClient.Gauge({ const monitor_status = new PrometheusClient.Gauge({
name: 'monitor_status', name: "monitor_status",
help: 'Monitor Status (1 = UP, 0= DOWN)', help: "Monitor Status (1 = UP, 0= DOWN)",
labelNames: commonLabels labelNames: commonLabels
}); });
@@ -33,7 +44,27 @@ class Prometheus {
} }
} }
update(heartbeat) { 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 { try {
monitor_status.set(this.monitorLabelValues, heartbeat.status) monitor_status.set(this.monitorLabelValues, heartbeat.status)
} catch (e) { } catch (e) {
@@ -41,7 +72,7 @@ class Prometheus {
} }
try { try {
if (typeof heartbeat.ping === 'number') { if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
} else { } else {
// Is it good? // Is it good?

View File

@@ -1,6 +1,7 @@
console.log("Welcome to Uptime Kuma") console.log("Welcome to Uptime Kuma");
console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug } = require("../src/util"); const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
console.log("Importing Node libraries") console.log("Importing Node libraries")
const fs = require("fs"); const fs = require("fs");
@@ -26,7 +27,7 @@ console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
debug("Importing Database"); debug("Importing Database");
@@ -39,7 +40,11 @@ const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const version = require("../package.json").version; const version = require("../package.json").version;
const hostname = process.env.HOST || args.host || "0.0.0.0"
// 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); const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version) console.info("Version: " + version)
@@ -87,17 +92,27 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
// Normal Router here // Normal Router here
app.use("/", express.static("dist")); // 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);
});
// Basic Auth Router here // Basic Auth Router here
// Prometheus API metrics /metrics // Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()) app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
// Universal Route Handler, must be at the end // Universal Route Handler, must be at the end
app.get("*", function(request, response, next) { app.get("*", async (_request, response) => {
response.end(indexHTML) response.send(indexHTML);
}); });
console.log("Adding socket handler") console.log("Adding socket handler")
@@ -114,11 +129,6 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
socket.emit("setup") socket.emit("setup")
} }
if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin")
await afterLogin(socket, await R.findOne("user", " username = 'admin' "))
}
socket.on("disconnect", () => { socket.on("disconnect", () => {
totalClient--; totalClient--;
}); });
@@ -139,8 +149,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
]) ])
if (user) { if (user) {
debug("afterLogin")
await afterLogin(socket, user) await afterLogin(socket, user)
debug("afterLogin ok")
callback({ callback({
ok: true, ok: true,
}) })
@@ -231,6 +245,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList; let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList; delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor) bean.import(monitor)
bean.user_id = socket.userID bean.user_id = socket.userID
await R.store(bean) await R.store(bean)
@@ -275,6 +292,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls; bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
await R.store(bean) await R.store(bean)
@@ -409,10 +428,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user && passwordHash.verify(password.currentPassword, user.password)) { if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ user.resetPassword(password.newPassword);
passwordHash.generate(password.newPassword),
socket.userID,
]);
callback({ callback({
ok: true, ok: true,
@@ -536,18 +552,44 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
callback(false); 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();
}); });
console.log("Init")
server.listen(port, hostname, () => { 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(); startMonitors();
}); });
})(); })();
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID, monitorID,
]) ])
@@ -598,15 +640,17 @@ async function afterLogin(socket, user) {
let monitorList = await sendMonitorList(socket) let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
}
sendNotificationList(socket) sendNotificationList(socket)
socket.emit("autoLogin") // 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) { async function getMonitorJSONList(userID) {
@@ -636,32 +680,22 @@ async function initDatabase() {
} }
console.log("Connecting to Database") console.log("Connecting to Database")
R.setup("sqlite", { await Database.connect();
filename: Database.path,
});
console.log("Connected") console.log("Connected")
// Patch the database // Patch the database
await Database.patch() await Database.patch()
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret", "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.") console.log("JWT secret is not found, generate one.");
jwtSecretBean = R.dispense("setting") jwtSecretBean = await initJWTSecret();
jwtSecretBean.key = "jwtSecret" console.log("Stored JWT secret into database");
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
await R.store(jwtSecretBean)
console.log("Stored JWT secret into database")
} else { } 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 there is no record in user table, it is a new Uptime Kuma instance, need to setup
@@ -721,15 +755,22 @@ async function startMonitors() {
let list = await R.find("monitor", " active = 1 ") let list = await R.find("monitor", " active = 1 ")
for (let monitor of list) { for (let monitor of list) {
monitor.start(io)
monitorList[monitor.id] = monitor; 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 * Send Heartbeat History list to socket
*/ */
async function sendHeartbeatList(socket, monitorID) { async function sendHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", ` let list = await R.find("heartbeat", `
monitor_id = ? monitor_id = ?
ORDER BY time DESC ORDER BY time DESC
@@ -748,6 +789,8 @@ async function sendHeartbeatList(socket, monitorID) {
} }
async function sendImportantHeartbeatList(socket, monitorID) { async function sendImportantHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", ` let list = await R.find("heartbeat", `
monitor_id = ? monitor_id = ?
AND important = 1 AND important = 1
@@ -757,6 +800,8 @@ async function sendImportantHeartbeatList(socket, monitorID) {
monitorID, monitorID,
]) ])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
socket.emit("importantHeartbeatList", monitorID, list) socket.emit("importantHeartbeatList", monitorID, list)
} }
@@ -771,11 +816,10 @@ async function shutdownFunction(signal) {
} }
await sleep(2000); await sleep(2000);
await Database.close(); await Database.close();
console.log("Stopped DB")
} }
function finalFunction() { function finalFunction() {
console.log("Graceful Shutdown Done") console.log("Graceful shutdown successfully!");
} }
gracefulShutdown(server, { gracefulShutdown(server, {
@@ -786,3 +830,9 @@ gracefulShutdown(server, {
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ... onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction, // finally function (sync) - e.g. for logging finally: finalFunction, // finally function (sync) - e.g. for logging
}); });
// 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

@@ -2,6 +2,27 @@ const tcpp = require("tcp-ping");
const Ping = require("./ping-lite"); const Ping = require("./ping-lite");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { debug } = require("../src/util"); 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) { exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -9,7 +30,7 @@ exports.tcping = function (hostname, port) {
address: hostname, address: hostname,
port: port, port: port,
attempts: 1, attempts: 1,
}, function(err, data) { }, function (err, data) {
if (err) { if (err) {
reject(err); reject(err);
@@ -24,15 +45,30 @@ exports.tcping = function (hostname, port) {
}); });
} }
exports.ping = function (hostname) { exports.ping = async (hostname) => {
return new Promise((resolve, reject) => { try {
const ping = new Ping(hostname); 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) { if (err) {
reject(err) reject(err);
} else if (ms === null) { } else if (ms === null) {
reject(new Error("timeout")) reject(new Error(stdout))
} else { } else {
resolve(Math.round(ms)) resolve(Math.round(ms))
} }
@@ -58,7 +94,7 @@ exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
]) ])
if (! bean) { if (!bean) {
bean = R.dispense("setting") bean = R.dispense("setting")
bean.key = key; bean.key = key;
} }
@@ -158,3 +194,32 @@ exports.checkCertificate = function (res) {
fingerprint, 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

@@ -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; 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 { .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); box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
@@ -29,10 +66,137 @@
background-color: $highlight; background-color: $highlight;
border-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

@@ -5,4 +5,14 @@ $link-color: #111;
$border-radius: 50rem; $border-radius: 50rem;
$highlight: #7ce8a4; $highlight: #7ce8a4;
$highlight-white: #e7faec; $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

@@ -22,15 +22,11 @@ export default {
computed: { computed: {
displayText() { displayText() {
if (this.value !== undefined && this.value !== "") { if (this.dateOnly) {
let format = "YYYY-MM-DD HH:mm:ss"; return this.$root.date(this.value);
if (this.dateOnly) { } else {
format = "YYYY-MM-DD"; return this.$root.datetime(this.value);
}
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
} }
return "";
}, },
}, },
} }

View File

@@ -7,7 +7,7 @@
class="beat" class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }" :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:style="beatStyle" :style="beatStyle"
:title="beat.msg" :title="getBeatTitle(beat)"
/> />
</div> </div>
</div> </div>
@@ -21,7 +21,10 @@ export default {
type: String, type: String,
default: "big", default: "big",
}, },
monitorId: Number, monitorId: {
type: Number,
required: true,
},
}, },
data() { data() {
return { return {
@@ -36,14 +39,15 @@ export default {
computed: { computed: {
beatList() { beatList() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
return this.$root.heartbeatList[this.monitorId] return this.$root.heartbeatList[this.monitorId]
}, },
shortBeatList() { shortBeatList() {
let placeholders = [] if (! this.beatList) {
return [];
}
let placeholders = [];
let start = this.beatList.length - this.maxBeat; let start = this.beatList.length - this.maxBeat;
@@ -113,6 +117,11 @@ export default {
unmounted() { unmounted() {
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
}, },
beforeMount() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
},
mounted() { mounted() {
if (this.size === "small") { if (this.size === "small") {
this.beatWidth = 5.6; this.beatWidth = 5.6;
@@ -129,11 +138,15 @@ export default {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)) this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
} }
}, },
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
}
}, },
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.wrap { .wrap {
@@ -150,6 +163,10 @@ export default {
&.empty { &.empty {
background-color: aliceblue; background-color: aliceblue;
.dark & {
background-color: #d0d3d5;
}
} }
&.down { &.down {
@@ -168,4 +185,10 @@ export default {
} }
} }
.dark {
.hp-bar-big .beat.empty{
background-color: #848484;
}
}
</style> </style>

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

@@ -21,8 +21,12 @@
<option value="gotify">Gotify</option> <option value="gotify">Gotify</option>
<option value="slack">Slack</option> <option value="slack">Slack</option>
<option value="pushover">Pushover</option> <option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option> <option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option> <option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
<option value="line">Line Messenger</option>
</select> </select>
</div> </div>
@@ -146,6 +150,11 @@
You can get this by going to Server Settings -> Integrations -> Create Webhook You can get this by going to Server Settings -> Integrations -> Create Webhook
</div> </div>
</div> </div>
<div class="mb-3">
<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>
<template v-if="notification.type === 'signal'"> <template v-if="notification.type === 'signal'">
@@ -229,6 +238,54 @@
</div> </div>
</template> </template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input 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'"> <template v-if="notification.type === 'pushover'">
<div class="mb-3"> <div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label> <label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
@@ -302,7 +359,7 @@
<p> <p>
Status: Status:
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span> <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">Read more</a></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> </p>
</div> </div>
</template> </template>
@@ -317,6 +374,36 @@
</div> </div>
</template> </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>
<div class="modal-footer"> <div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
@@ -490,3 +577,13 @@ export default {
}, },
} }
</script> </script>
<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

@@ -1,109 +1,127 @@
<template> <template>
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection"> <div :class="classes">
<div class="container-fluid"> <div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
{{ $root.connectionErrorMsg }} <div class="container-fluid">
{{ $root.connectionErrorMsg }}
</div>
</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> </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 mt-3 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" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="tachometer-alt" /></div>
Dashboard
</router-link>
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile">
<div><font-awesome-icon icon="list" /></div>
List
</a>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="plus" /></div>
Add
</router-link>
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="cog" /></div>
Settings
</router-link>
</nav>
</template> </template>
<script> <script>
import Login from "../components/Login.vue"; import Login from "../components/Login.vue";
export default { export default {
components: { components: {
Login, Login,
}, },
data() { data() {
return {} return {}
}, },
computed: {},
computed: {
// Theme or Mobile
classes() {
const classes = {};
classes[this.$root.theme] = true;
classes["mobile"] = this.$root.isMobile;
return classes;
}
},
watch: { watch: {
$route (to, from) { $route(to, from) {
this.init(); this.init();
}, },
}, },
mounted() { mounted() {
this.init(); this.init();
}, },
methods: { methods: {
init() { init() {
if (this.$route.name === "root") { if (this.$route.name === "root") {
this.$router.push("/dashboard") this.$router.push("/dashboard")
} }
}, },
}, },
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.bottom-nav { .bottom-nav {
@@ -141,6 +159,10 @@ export default {
} }
} }
main {
min-height: calc(100vh - 160px)
}
.title { .title {
font-weight: bold; font-weight: bold;
} }
@@ -159,9 +181,24 @@ footer {
color: #AAA; color: #AAA;
font-size: 13px; font-size: 13px;
margin-top: 10px; margin-top: 10px;
margin-bottom: 30px; padding-bottom: 30px;
margin-left: 10px; margin-left: 10px;
text-align: center; text-align: center;
} }
.dark {
header {
background-color: #161B22;
border-bottom-color: #161B22 !important;
span {
color: #F0F6FC;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style> </style>

View File

@@ -9,12 +9,18 @@ import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue"; import Layout from "./layouts/Layout.vue";
import socket from "./mixins/socket"; 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 Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue"; import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue"; import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue"; import EditMonitor from "./pages/EditMonitor.vue";
import Settings from "./pages/Settings.vue"; import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
import List from "./pages/List.vue";
import { appName } from "./util.ts";
const routes = [ const routes = [
{ {
@@ -49,6 +55,10 @@ const routes = [
path: "/add", path: "/add",
component: EditMonitor, component: EditMonitor,
}, },
{
path: "/list",
component: List,
},
], ],
}, },
{ {
@@ -76,7 +86,15 @@ const router = createRouter({
const app = createApp({ const app = createApp({
mixins: [ mixins: [
socket, socket,
theme,
mobile,
datetime
], ],
data() {
return {
appName: appName
}
},
render: () => h(App), render: () => h(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,4 +1,3 @@
import dayjs from "dayjs";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast()
@@ -17,7 +16,6 @@ export default {
connectCount: 0, connectCount: 0,
}, },
remember: (localStorage.remember !== "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. 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, loggedIn: false,
monitorList: { }, monitorList: { },
@@ -27,9 +25,7 @@ export default {
uptimeList: { }, uptimeList: { },
certInfoList: {}, certInfoList: {},
notificationList: [], notificationList: [],
windowWidth: window.innerWidth, connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
showListMobile: false,
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
} }
}, },
@@ -189,14 +185,6 @@ export default {
methods: { methods: {
cancelActiveList() {
this.$root.showListMobile = false;
},
onResize() {
this.windowWidth = window.innerWidth;
},
storage() { storage() {
return (this.remember) ? localStorage : sessionStorage; return (this.remember) ? localStorage : sessionStorage;
}, },
@@ -270,19 +258,6 @@ export default {
computed: { computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
timezone() {
if (this.userTimezone === "auto") {
return dayjs.tz.guess()
}
return this.userTimezone
},
lastHeartbeatList() { lastHeartbeatList() {
let result = {} let result = {}

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,31 +1,13 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-xl-4"> <div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile"> <div>
<router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link> <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
</div>
<div v-if="showList" 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 }" @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> </div>
<MonitorList />
</div> </div>
<div class="col-12 col-md-7 col-xl-8"> <div class="col-12 col-md-7 col-xl-8">
<router-view /> <router-view />
</div> </div>
@@ -35,102 +17,21 @@
<script> <script>
import HeartbeatBar from "../components/HeartbeatBar.vue"; import MonitorList from "../components/MonitorList.vue";
import Uptime from "../components/Uptime.vue";
export default { export default {
components: { components: {
Uptime, MonitorList,
HeartbeatBar,
}, },
data() { 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> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss";
.container-fluid { .container-fluid {
width: 98% 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;
}
}
}
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
</style> </style>

View File

@@ -1,78 +1,79 @@
<template> <template>
<div v-if="$route.name === 'DashboardHome'"> <transition name="slide-fade" appear>
<h1 class="mb-3"> <div v-if="$route.name === 'DashboardHome'">
Quick Stats <h1 class="mb-3">
</h1> Quick Stats
</h1>
<div class="shadow-box big-padding text-center"> <div class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>Up</h3> <h3>Up</h3>
<span class="num">{{ stats.up }}</span> <span class="num">{{ stats.up }}</span>
</div>
<div 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>
<div class="col"> <div v-if="false" class="row">
<h3>Down</h3> <div class="col-3">
<span class="num text-danger">{{ stats.down }}</span> <h3>Uptime</h3>
</div> <p>(24-hour)</p>
<div class="col"> <span class="num" />
<h3>Unknown</h3> </div>
<span class="num text-secondary">{{ stats.unknown }}</span> <div class="col-3">
</div> <h3>Uptime</h3>
<div class="col"> <p>(30-day)</p>
<h3>Pause</h3> <span class="num" />
<span class="num text-secondary">{{ stats.pause }}</span> </div>
</div> </div>
</div> </div>
<div v-if="false" class="row">
<div class="col-3"> <div class="shadow-box" style="margin-top: 25px;overflow-x: scroll">
<h3>Uptime</h3> <table class="table table-borderless table-hover">
<p>(24-hour)</p> <thead>
<span class="num" /> <tr>
</div> <th>Name</th>
<div class="col-3"> <th>Status</th>
<h3>Uptime</h3> <th>DateTime</th>
<p>(30-day)</p> <th>Message</th>
<span class="num" /> </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>
</div> </div>
</transition>
<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, 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>
<router-view ref="child" /> <router-view ref="child" />
</template> </template>
@@ -169,7 +170,7 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars"; @import "../assets/vars";
.num { .num {

View File

@@ -1,160 +1,179 @@
<template> <template>
<h1> {{ monitor.name }}</h1> <transition name="slide-fade" appear>
<p class="url"> <div>
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a> <h1> {{ monitor.name }}</h1>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span> <p class="url">
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<br> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span> <span v-if="monitor.type === 'keyword'">
</span> <br>
</p> <span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
<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>
<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="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 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> </span>
</div> </p>
</div>
</div>
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center"> <div class="functions">
<div class="row"> <button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<div class="col"> <font-awesome-icon icon="pause" /> Pause
<h4>Certificate Info</h4> </button>
<table class="text-start"> <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>
<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="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> <tbody>
<tr class="my-3"> <tr v-for="(beat, index) in displayedRecords" :key="index">
<td class="px-3"> <td><Status :status="beat.status" /></td>
Valid: <td><Datetime :value="beat.time" /></td>
</td> <td>{{ beat.msg }}</td>
<td>{{ certInfo.valid }}</td>
</tr> </tr>
<tr class="my-3">
<td class="px-3"> <tr v-if="importantHeartBeatList.length === 0">
Valid To: <td colspan="3">
No important events
</td> </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> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div> </div>
<Confirm ref="confirmPause" @yes="pauseMonitor">
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> </transition>
<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>
</template> </template>
<script> <script>
import { defineAsyncComponent } from "vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
@@ -164,6 +183,7 @@ import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue"; import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3"; import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
export default { export default {
components: { components: {
@@ -174,6 +194,7 @@ export default {
Confirm, Confirm,
Status, Status,
Pagination, Pagination,
PingChart,
}, },
data() { data() {
return { return {
@@ -181,6 +202,7 @@ export default {
perPage: 25, perPage: 25,
heartBeatList: [], heartBeatList: [],
toggleCertInfoBox: false, toggleCertInfoBox: false,
showPingChartBox: true,
} }
}, },
computed: { computed: {
@@ -306,6 +328,41 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../assets/vars.scss"; @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 { .url {
color: $primary; color: $primary;
margin-bottom: 20px; margin-bottom: 20px;
@@ -352,4 +409,14 @@ table {
margin: 20px 0; margin: 20px 0;
} }
} }
.keyword {
color: black;
}
.dark {
.keyword {
color: $dark-font-color;
}
}
</style> </style>

View File

@@ -1,143 +1,178 @@
<template> <template>
<h1 class="mb-3"> <transition name="slide-fade" appear>
{{ pageName }} <div>
</h1> <h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2 class="mb-2">General</h2>
<div class="mb-3"> <div class="my-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">Monitor Type</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http"> <option value="http">
HTTP(s) HTTP(s)
</option> </option>
<option value="port"> <option value="port">
TCP Port TCP Port
</option> </option>
<option value="ping"> <option value="ping">
Ping Ping
</option> </option>
<option value="keyword"> <option value="keyword">
HTTP(s) - Keyword HTTP(s) - Keyword
</option> </option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">URL</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div v-if="monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">Keyword</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
Search keyword in plain html or JSON response and it is case-sensitive 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>
</div>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3"> <div class="col-md-6">
<label for="hostname" class="form-label">Hostname</label> <div v-if="$root.isMobile" class="mt-3" />
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'port' " class="mb-3"> <h2 class="mb-2">Notifications</h2>
<label for="port" class="form-label">Port</label> <p v-if="$root.notificationList.length === 0">
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> Not available, please setup.
</div> </p>
<div class="mb-3"> <div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div>
<div class="mb-3"> <label class="form-check-label" :for=" 'notification' + notification.id">
<label for="maxRetries" class="form-label">Retries</label> {{ notification.name }}
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1"> <a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
<div class="form-text"> </label>
Maximum retries before the service is marked as down and a notification is sent </div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div> </div>
</div> </div>
<h2>Advanced</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites
</label>
</div>
<div class="mb-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down">
Upside Down Mode
</label>
<div class="form-text">
Flip the status upside down. If the service is reachable, it is DOWN.
</div>
</div>
<div>
<button class="btn btn-primary" type="submit" :disabled="processing">
Save
</button>
</div>
</div> </div>
</form>
<div class="col-md-6"> <NotificationDialog ref="notificationDialog" />
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div>
</div> </div>
</form> </transition>
<NotificationDialog ref="notificationDialog" />
</template> </template>
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog, NotificationDialog,
VueMultiselect,
}, },
data() { data() {
return { return {
processing: false, processing: false,
monitor: { monitor: {
notificationIDList: {}, notificationIDList: {},
}, },
acceptedStatusCodeOptions: [],
} }
}, },
computed: { computed: {
pageName() { pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit" return (this.isAdd) ? "Add New Monitor" : "Edit"
@@ -150,12 +185,26 @@ export default {
}, },
}, },
watch: { watch: {
"$route.fullPath" () { "$route.fullPath"() {
this.init(); this.init();
}, },
}, },
mounted() { mounted() {
this.init(); 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: { methods: {
init() { init() {
@@ -170,6 +219,8 @@ export default {
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
} }
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@@ -209,6 +260,40 @@ export default {
} }
</script> </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> <style scoped>
.shadow-box { .shadow-box {
padding: 20px; 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,104 +1,159 @@
<template> <template>
<h1 class="mb-3"> <transition name="slide-fade" appear>
Settings <div>
</h1> <h1 v-show="show" class="mb-3">
Settings
</h1>
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2 class="mb-2">General</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<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> <form class="mb-3" @submit.prevent="saveGeneral">
<button class="btn btn-primary" type="submit">
Save
</button>
</div>
</form>
<template v-if="loaded">
<template v-if="! settings.disableAuth">
<h2>Change Password</h2>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3"> <div class="mb-3">
<label for="current-password" class="form-label">Current Password</label> <label for="timezone" class="form-label">Theme</label>
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
<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>
<div class="mb-3"> <div class="mb-3">
<label for="new-password" class="form-label">New Password</label> <label class="form-label">Theme - Heartbeat Bar</label>
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required> <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>
<div class="mb-3"> <div class="mb-3">
<label for="repeat-new-password" class="form-label">Repeat New Password</label> <label for="timezone" class="form-label">Timezone</label>
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required> <select id="timezone" v-model="$root.userTimezone" class="form-select">
<div class="invalid-feedback"> <option value="auto">
The repeat password does not match. 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> </div>
<div> <div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
Update Password Save
</button> </button>
</div> </div>
</form> </form>
</template>
<h2>Advanced</h2> <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"> <div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button> <label for="new-password" class="form-label">New Password</label>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button> <input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button> </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>
</template>
<div class="notification-list col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<p v-else>
Please assign a notification to monitor(s) to get it to work.
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<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>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div>
</div> </div>
<div class="col-md-6"> <NotificationDialog ref="notificationDialog" />
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2> <Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
<p v-if="$root.notificationList.length === 0"> <p>Are you sure want to <strong>disable auth</strong>?</p>
Not available, please setup. <p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
</p> <p>Please use it carefully.</p>
<p v-else> </Confirm>
Please assign a notification to monitor(s) to get it to work.
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div> </div>
</div> </transition>
<NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</Confirm>
</template> </template>
<script> <script>
@@ -123,7 +178,7 @@ export default {
return { return {
timezoneList: timezoneList(), timezoneList: timezoneList(),
guessTimezone: dayjs.tz.guess(), guessTimezone: dayjs.tz.guess(),
show: true,
invalidPassword: false, invalidPassword: false,
password: { password: {
currentPassword: "", currentPassword: "",
@@ -150,7 +205,7 @@ export default {
saveGeneral() { saveGeneral() {
localStorage.timezone = this.$root.userTimezone; localStorage.timezone = this.$root.userTimezone;
toast.success("Saved.") this.saveSettings();
}, },
savePassword() { savePassword() {
@@ -171,6 +226,11 @@ export default {
loadSettings() { loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => { this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data; this.settings = res.data;
if (this.settings.searchEngineIndex === undefined) {
this.settings.searchEngineIndex = false;
}
this.loaded = true; this.loaded = true;
}) })
}, },
@@ -201,8 +261,29 @@ export default {
} }
</script> </script>
<style scoped> <style lang="scss" scoped>
.shadow-box { @import "../assets/vars.scss";
padding: 20px;
.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> </style>

View File

@@ -1,6 +1,10 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0; 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.DOWN = 0;
exports.UP = 1; exports.UP = 1;
exports.PENDING = 2; exports.PENDING = 2;
@@ -27,8 +31,40 @@ function ucfirst(str) {
} }
exports.ucfirst = ucfirst; exports.ucfirst = ucfirst;
function debug(msg) { function debug(msg) {
if (process.env.NODE_ENV === "development") { if (exports.isDev) {
console.log(msg); console.log(msg);
} }
} }
exports.debug = debug; 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;

View File

@@ -1,8 +1,14 @@
// @ts-nocheck
// Common Util for frontend and backend // Common Util for frontend and backend
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
// Need to run "tsc" to compile if there are any changes. // 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 DOWN = 0;
export const UP = 1; export const UP = 1;
export const PENDING = 2; export const PENDING = 2;
@@ -37,7 +43,63 @@ export function ucfirst(str) {
} }
export function debug(msg) { export function debug(msg) {
if (process.env.NODE_ENV === "development") { if (isDev) {
console.log(msg) 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

View File

@@ -1,8 +1,11 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"target": "ES2018", "target": "es2018",
"module": "commonjs", "module": "commonjs",
"lib": [
"es2020"
],
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"sourceMap": false, "sourceMap": false,

View File

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