Compare commits

..

411 Commits
1.0.9 ... 1.5.3

Author SHA1 Message Date
LouisLam
6a603203cc update to 1.5.3 2021-09-04 15:07:32 +08:00
LouisLam
4b8e7fcffc update "build-docker" to build both platforms 2021-09-04 15:03:26 +08:00
LouisLam
7fd12f5485 rename nl_NL.js to nl-NL.js 2021-09-04 14:42:55 +08:00
LouisLam
0d87a6dfea update vue to 3.2.8 2021-09-04 14:41:45 +08:00
Louis Lam
2082c3f68a Merge pull request #313 from xoniq/lang-nl
Add Dutch/Nederlands language (nl_NL)
2021-09-04 00:05:05 +08:00
Louis Lam
ea9f434553 Merge pull request #328 from Ponkhy/german-language
Small german language adjustments
2021-09-03 23:06:32 +08:00
Louis Lam
fa1216c198 Merge pull request #329 from dhfhfk/New-string
Add missing strings for Korean
2021-09-03 23:05:48 +08:00
dhfhfk
6d8aa20fc6 Add missing string for Korean
confirmDisableAuth, new string
2021-09-03 23:18:18 +09:00
Ponkhy
5430da955a Small german language adjustments 2021-09-03 15:54:14 +02:00
J Posthuma
e6e2b0ebf8 Update nl_NL.js 2021-09-03 13:42:14 +02:00
J Posthuma
445dc486be Merge branch 'master' into lang-nl 2021-09-03 13:41:10 +02:00
Louis Lam
d2151737c1 Update README.md 2021-09-03 17:09:11 +08:00
Louis Lam
78fc6de542 Merge pull request #325 from Saibamen/patch-1
Update `README.md` - Open Collective URL
2021-09-03 15:49:23 +08:00
Adam Stachowicz
c15e6631ae Update README.md
Open Collective URL
2021-09-03 09:16:54 +02:00
Louis Lam
f84135ca67 Merge pull request #322 from chakflying/patch-6
Fix: Fix Notification "Test" btn styling
2021-09-03 14:34:39 +08:00
LouisLam
80eca886ce Merge branch 'add-serbian-latin'
# Conflicts:
#	src/main.js
#	src/pages/Settings.vue
2021-09-03 14:32:12 +08:00
Louis Lam
274a9050c0 Merge pull request #321 from dusansimic/add-serbian-cyrillic
Add Serbian Cyrillic locale
2021-09-03 14:00:43 +08:00
Louis Lam
1f4ad938b1 Merge pull request #323 from xinac721/master
Update Chinese Translation(更新简体中文语言翻译)
2021-09-03 13:55:30 +08:00
新逸Cary
312aedf391 Update Chinese Translation(更新简体中文语言翻译) 2021-09-03 13:33:26 +08:00
Louis Lam
4f28bfbde3 Merge pull request #309 from Saibamen/updateDependencies
Update dependencies
2021-09-03 12:56:55 +08:00
Louis Lam
ee76ab4ec2 Merge pull request #317 from AtaxyaNetwork/master
change translation FR
2021-09-03 12:56:11 +08:00
Nelson Chan
18616ee590 Fix: Fix Notification "Test" btn styling 2021-09-03 12:05:49 +08:00
Dušan Simić
19a4d570ec Fix word capitalization 2021-09-03 02:24:31 +02:00
Dušan Simić
79fda8f442 Add Serbian Latin locale 2021-09-03 02:20:01 +02:00
Dušan Simić
53a14cf4f5 Add Serbian Cyrilic locale 2021-09-03 02:06:26 +02:00
cecile
2241d8817f change translation FR 2021-09-02 23:41:47 +02:00
Louis Lam
3831dfe0b9 Update README.md 2021-09-03 02:55:36 +08:00
Louis Lam
fdde862549 Merge pull request #316 from aictur/feature/es-ES
Add spanish language and update Readme
2021-09-02 23:00:36 +08:00
LouisLam
e31be8caf5 demo mode 2021-09-02 22:52:20 +08:00
LouisLam
60f2f08cea add demo db 2021-09-02 22:39:04 +08:00
LouisLam
b1647a310e add demo db 2021-09-02 22:37:51 +08:00
Victor M. Vicente Cuevas
23f1a73fc8 Add language to src/main and update README 2021-09-02 16:31:45 +02:00
Victor M. Vicente Cuevas
d2bf2a551d Add spanish language and update Readme 2021-09-02 16:23:07 +02:00
LouisLam
f23ecef636 add missing cert parameters 2021-09-02 21:16:04 +08:00
LouisLam
51cf2ff6f9 add missing cert parameters 2021-09-02 21:13:59 +08:00
LouisLam
b30b1d3a52 create data dir before copy 2021-09-02 21:11:20 +08:00
LouisLam
582e14098d create data dir before copy 2021-09-02 21:10:18 +08:00
LouisLam
6e3e2fc85c fix db path 2021-09-02 21:08:00 +08:00
LouisLam
b604807cfe create data dir if not exists 2021-09-02 20:42:55 +08:00
LouisLam
3ee13bddd1 dash style for args 2021-09-02 20:36:52 +08:00
LouisLam
c74986647e allow changing data dir 2021-09-02 20:27:18 +08:00
LouisLam
dac410c850 Merge remote-tracking branch 'origin/master' 2021-09-02 20:20:47 +08:00
LouisLam
b88b357b55 add support for https 2021-09-02 20:18:27 +08:00
Louis Lam
9a5cd5172b Merge pull request #314 from xinac721/master
Chinese Translation (from zh-CN.js)
2021-09-02 20:14:05 +08:00
新逸Cary
941788db49 Chinese Translation (from zh-CN.js) 2021-09-02 18:33:09 +08:00
Jelle Posthuma
99725aabe7 Add Dutch/Nederlands language (nl_NL) 2021-09-02 11:10:54 +02:00
Louis Lam
9d41da4aa2 Merge pull request #311 from xinac721/master
Chinese Translation (from zh-CN.js)
2021-09-02 15:59:06 +08:00
新逸Cary
a0f372e946 Chinese Translation (from zh-CN.js) 2021-09-02 15:34:46 +08:00
新逸Cary
877ad1438f Merge branch 'louislam:master' into master 2021-09-02 15:25:09 +08:00
LouisLam
9116654a33 update language files and run eslint on these 2021-09-02 15:21:46 +08:00
新逸Cary
dbc70bc5ed Merge pull request #1 from louislam/master
update
2021-09-02 14:33:34 +08:00
Louis Lam
a29b65e801 Merge pull request #308 from DX37/translation-ru
Add Russian translation
2021-09-02 12:56:12 +08:00
Louis Lam
bd9568ce5b Merge pull request #310 from Saibamen/more_i18n
Make `Resp. Time (ms)` and `N/A` i18n
2021-09-02 12:55:11 +08:00
Adam Stachowicz
7a109689d9 Make Resp. Time (ms) and N/A i18n 2021-09-01 21:17:50 +02:00
Adam Stachowicz
e7929f461d Update dependencies 2021-09-01 20:53:12 +02:00
DX37
a2cf7f394e Add Russian translation 2021-09-02 01:37:01 +07:00
Louis Lam
e1f378ee6c Update README.md 2021-09-02 02:11:44 +08:00
LouisLam
eeb00a5511 fix data type 2021-09-02 01:56:02 +08:00
Louis Lam
129dc3324b Merge pull request #307 from dhfhfk/master
Added Korean Language
2021-09-02 01:17:54 +08:00
Louis Lam
268d2e2353 Merge branch 'master' into master 2021-09-02 01:17:48 +08:00
Louis Lam
7d2cf0ee57 Merge pull request #304 from LeviSnoot/master
Added Swedish Language
2021-09-02 01:17:10 +08:00
오로라
2d408732db Update main.js
Prevent conflict with #304 (Added Swedish Language)
2021-09-02 01:01:18 +09:00
dhfhfk
0cc5053f14 Update ko-KR.js 2021-09-02 00:35:50 +09:00
dhfhfk
1c4e5b79be Create ko-KR.js 2021-09-02 00:30:08 +09:00
dhfhfk
4aae402b36 Update main.js 2021-09-02 00:29:01 +09:00
Levi
dce2ba8f9f Update sv-SE.js 2021-09-01 16:41:51 +02:00
Levi
ed5c75282c Update main.js 2021-09-01 16:38:05 +02:00
Levi
00ac560bd6 Create sv-SE.js 2021-09-01 16:30:08 +02:00
Louis Lam
8ea4dec5a0 Merge pull request #303 from Saibamen/patch-1
Update translation docs
2021-09-01 22:08:44 +08:00
Adam Stachowicz
3006d13aee Update translation docs 2021-09-01 15:37:56 +02:00
LouisLam
c7e6cb9f37 update language files 2021-09-01 20:10:54 +08:00
LouisLam
9b82bb248e Merge branch 'master' into kometchtech_patch-1
# Conflicts:
#	src/main.js
2021-09-01 19:46:44 +08:00
Louis Lam
e55cf65f28 Merge pull request #291 from Ponkhy/master
Added translation to Setup.vue
2021-09-01 19:45:20 +08:00
Louis Lam
f24002838c Merge branch 'master' into master 2021-09-01 19:45:06 +08:00
Louis Lam
b4fd4f7d90 Merge pull request #301 from Lrss/master
Added Danish language
2021-09-01 19:44:01 +08:00
kometchtech
3a3f1762ac Update ja.js
* Perform translation for added keys.
2021-09-01 19:04:35 +09:00
Lars Sørensen
6376d4eb92 Translated to Danish 2021-09-01 11:51:22 +02:00
LouisLam
e37cf9b1a7 add missing and add Japanese to the list 2021-09-01 17:32:41 +08:00
LouisLam
3b0191210b Merge branch 'master' into kometchtech_patch-1 2021-09-01 17:28:49 +08:00
LouisLam
923d325b44 add language file preparation script 2021-09-01 17:10:12 +08:00
LouisLam
ad38e61c26 add language file preparation script 2021-09-01 17:08:27 +08:00
LouisLam
22095c09b1 add translation guide 2021-09-01 15:49:35 +08:00
LouisLam
a5e62141d5 update to 1.5.2 2021-09-01 15:04:31 +08:00
LouisLam
e4b76717be revert back to node-sqlite3 2021-09-01 15:02:04 +08:00
LouisLam
bc70ecfba7 fix install script 2021-09-01 14:56:19 +08:00
LouisLam
f1238ab762 update to 1.5.1 2021-09-01 14:41:06 +08:00
LouisLam
cd1a3a2fb9 revert back to node-sqlite3, as better-sqlite3 causes a lot of installation problems 2021-09-01 14:33:00 +08:00
kometchtech
25db162721 Create ja.js
* Created a Japanese language file.
2021-09-01 13:23:44 +09:00
LouisLam
3e7bb355fd update to 1.5.0 2021-08-31 23:10:12 +08:00
LouisLam
697fa6bdfd fix discord notification appended port unexpectedly 2021-08-31 22:15:02 +08:00
LouisLam
527e0c3444 raise the ping timeout from 2s to 10s (avoid #294) 2021-08-31 22:14:33 +08:00
LouisLam
a41534ca60 no declare vars with comma, one line only one statement 2021-08-31 22:08:05 +08:00
LouisLam
fa549cb80e fix npm7 broken dependencies 2021-08-31 20:36:17 +08:00
LouisLam
0127d5102e Merge remote-tracking branch 'origin/master' 2021-08-31 20:03:24 +08:00
LouisLam
ec731d174d Merge branch 'MichelBaie_master' 2021-08-31 20:02:39 +08:00
LouisLam
0d65918a6a change bcrypt to bcryptjs, use my own prebuilt better-sqlite3, supports more prebuilt 2021-08-31 19:56:44 +08:00
Louis Lam
21db31bb30 Update README.md 2021-08-30 21:56:33 +08:00
Louis Lam
3ee0addb1f Merge pull request #281 from MichelBaie/master
Traduction pour le language France (FR Translation)
2021-08-30 20:09:55 +08:00
LouisLam
8c5d1945be add fr to the list 2021-08-30 19:46:10 +08:00
Tristan
bb799163e8 French Translation (from zh-HK.js) 2021-08-30 11:11:22 +02:00
LouisLam
bf29f28726 send stats only if there is at least one client in the room 2021-08-30 14:55:33 +08:00
LouisLam
22db9c9b8a Merge remote-tracking branch 'origin/master' into scroll-into-view 2021-08-30 14:00:34 +08:00
LouisLam
96ff70faaa reduce shifting when click the monitor list item (still shift in some cases, but acceptable) 2021-08-30 13:09:24 +08:00
henrygd
2776f942ab fix monitor list jumping to top on route change 2021-08-29 15:28:43 -07:00
Ponkhy
14b9afb400 Added translation to Setup.vue 2021-08-29 23:07:58 +02:00
LouisLam
3ad736692f improve monitor list 2021-08-30 02:22:49 +08:00
LouisLam
e644a1e36f Merge branch 'master' into scroll-into-view 2021-08-30 00:46:26 +08:00
Louis Lam
78b7e36a38 Update domain name 2021-08-29 19:21:13 +08:00
Louis Lam
f30232c35d Create CNAME 2021-08-29 19:15:40 +08:00
Louis Lam
257a4ee994 Update patch7.sql 2021-08-29 18:58:05 +08:00
Louis Lam
fd1ba46d3d Merge pull request #287 from Ponkhy/dns-monitor
[Before 1.5.0] Show latest dns result in Details.vue
2021-08-29 11:17:35 +08:00
LouisLam
ada6606217 move the new sql to patch8.sql 2021-08-29 11:16:06 +08:00
Ponkhy
dd877cfc70 Added translation to pause monitor confirmation 2021-08-29 04:03:55 +02:00
Ponkhy
93edb8817d More uniform look 2021-08-29 03:57:26 +02:00
Ponkhy
858affa808 Removed useless database query 2021-08-28 21:29:24 +02:00
Louis Lam
2bb182b2b4 Create FUNDING.yml 2021-08-29 03:28:40 +08:00
Ponkhy
303adbf9b1 Show latest dns result in Details.vue 2021-08-28 21:20:25 +02:00
Louis Lam
712da02324 Merge pull request #286 from arnishb/fix-readme
fix-readme
2021-08-28 19:32:01 +08:00
Arnish Baruah
27a4dbb722 fix-readme 2021-08-28 16:40:32 +05:30
Louis Lam
0e676060f2 Update CONTRIBUTING.md 2021-08-28 13:46:47 +08:00
Louis Lam
a14c40f9bb move k8s to advanced installation section 2021-08-28 12:33:10 +08:00
Louis Lam
309fbfa094 Update README.md 2021-08-28 12:32:35 +08:00
Louis Lam
46dcd31142 Update README.md 2021-08-28 12:29:57 +08:00
LouisLam
59e9315647 show dns type and hostname in Details.vue 2021-08-28 11:48:34 +08:00
LouisLam
91e82bd12c set default value for dns resolve type and code refactor 2021-08-28 11:46:26 +08:00
LouisLam
d4dd650bfe translate for zh-hk 2021-08-28 11:20:02 +08:00
Louis Lam
354ee1a58c Merge pull request #283 from Ponkhy/german-language
Added German language for DNS Monitor
2021-08-28 11:10:39 +08:00
henrygd
2901a38628 add position: sticky to monitor details 2021-08-27 15:28:01 -07:00
Ponkhy
6464207f4b Added German language for DNS Monitor 2021-08-27 18:41:58 +02:00
Louis Lam
7652b4849a Merge pull request #238 from Ponkhy/dns-monitor
Added DNS Monitor Type
2021-08-28 00:21:10 +08:00
LouisLam
03b4086372 add CAA test and remove some files added by mistake 2021-08-27 23:57:42 +08:00
Ponkhy
acd5cf63fd Removed wrong PTR answer 2021-08-27 17:41:22 +02:00
Ponkhy
177af2d588 Added more dns types to simple-dns-server 2021-08-27 17:32:50 +02:00
Tristan
b46b214175 Traduction pour le language France (FR Translation) 2021-08-27 11:12:22 +02:00
Louis Lam
d2f0a15076 Merge pull request #264 from antiseptikk/master
feat: add rocket.chat notification
2021-08-26 23:34:07 +08:00
LouisLam
2d50d24276 more translation for zh-HK 2021-08-26 23:32:25 +08:00
Louis Lam
34b6976a03 Merge pull request #269 from Ponkhy/german-language
Added German language and new translations
2021-08-26 23:14:05 +08:00
Louis Lam
d60c11e845 Merge pull request #260 from Thiesjoo/discord-notification
Discord notification updates
2021-08-26 23:03:32 +08:00
LouisLam
890aea8756 Merge branch 'duzun_patch-1' 2021-08-26 23:02:10 +08:00
LouisLam
c55f2b93f7 change to pm2 start server/server.js directly due to (#273) 2021-08-26 22:55:44 +08:00
Louis Lam
d4cf838af7 Merge pull request #273 from duzun/patch-1
Dockerfile: Avoid keeping npm in RAM
2021-08-26 22:45:59 +08:00
LouisLam
508586fcfd add codesandbox config for demo 2021-08-26 20:36:58 +08:00
Dumitru Uzun
feb0feda76 Dockerfile: Avoid keeping npm in RAM
By running node directly, we save some RAM. In my case npm consumes 300MB and does nothing, just waits for the node process to exit.
On small VPSes 300MB is a lot!
2021-08-26 12:53:57 +03:00
Ponkhy
34ca5c5c15 Merge branch 'master' of https://github.com/louislam/uptime-kuma 2021-08-26 02:43:35 +02:00
Ponkhy
2b8c5e2e65 Added German Language 2021-08-26 02:43:26 +02:00
Thomas Ferney
44d9967cfb feat: add rocket.chat notification 2021-08-25 21:01:29 +02:00
LouisLam
8318c2e8ff add a simple dns server for testing, and disable ipRegex for dev only (need to input port) 2021-08-26 01:50:27 +08:00
LouisLam
46ac753c46 Merge branch 'master' into dns-monitor 2021-08-26 01:05:46 +08:00
Thies
72740ba477 Update the styling to better match existing styles
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-25 14:08:41 +02:00
Thies Nieborg
5d438ca2b6 Discord notification URL now also represents non http services 2021-08-25 13:07:52 +02:00
Ponkhy
02a12e68b8 Used ALTER TABLE instead of rebuilding the table 2021-08-25 12:45:47 +02:00
Louis Lam
a7cd70f7de Merge pull request #253 from chakflying/chart-fix
Fix: More Chart improvements
2021-08-25 17:35:25 +08:00
Louis Lam
26db0471da Merge pull request #254 from RasHas/master
added mattermost notification support
2021-08-25 17:06:07 +08:00
Ponkhy
d313a06d5c Optimizations for output handling 2021-08-25 09:31:42 +02:00
LouisLam
4c1f2f85f8 manual fix stylelint 2021-08-25 12:25:19 +08:00
Rashad
23851ef539 added mattermost notification support 2021-08-24 21:19:21 +03:00
LouisLam
397fd12081 remove unused import 2021-08-25 01:26:10 +08:00
LouisLam
564bc96735 eslint: camelcase rule do not check properties, because it could be database field name 2021-08-25 01:25:57 +08:00
LouisLam
682e4d45e2 eslint for notification.js 2021-08-25 01:21:06 +08:00
LouisLam
f96d792fa1 fix patch database using better-sqlite3 2021-08-25 01:11:19 +08:00
Nelson Chan
2d20634738 Chore: Add comments, improve performance & styling 2021-08-25 01:03:20 +08:00
Nelson Chan
9442c3fa05 Fix: Fix chart bars to be full width 2021-08-25 01:02:58 +08:00
LouisLam
302d2665d2 run stylelint for the project 2021-08-24 23:38:25 +08:00
LouisLam
2b68be52b0 Merge branch 'clean-mobile-table' 2021-08-24 23:22:33 +08:00
LouisLam
393c4fb1a7 update stylelint 2021-08-24 23:22:04 +08:00
Louis Lam
13f6c79e79 Merge pull request #236 from Ponkhy/clean-mobile-table
Added clean monitor table for smaller screens
2021-08-24 23:21:22 +08:00
LouisLam
ca69d06e0d update border color 2021-08-24 22:27:26 +08:00
LouisLam
d8d428907e Merge branch 'master' into clean-mobile-table 2021-08-24 21:55:16 +08:00
Ponkhy
a17c14ea1c Centered title, badge and datetime 2021-08-24 13:24:35 +02:00
LouisLam
28a51d806b translate to Traditional Chinese (Hong Kong) 2021-08-24 18:26:44 +08:00
Ponkhy
6eead46fa6 Merge branch 'dns-monitor' of https://github.com/Ponkhy/uptime-kuma into dns-monitor 2021-08-24 11:47:24 +02:00
Ponkhy
44d9fa63f0 Adjusted the output for A and AAAA records 2021-08-24 11:47:12 +02:00
LouisLam
dd4c00eed3 add vue i18n 2021-08-24 16:44:58 +08:00
LouisLam
752ac05149 Merge remote-tracking branch 'origin/master' 2021-08-24 15:46:48 +08:00
LouisLam
14652c9b5f Remove unused variables 2021-08-24 15:46:22 +08:00
Louis Lam
054186370e add source of ipRegex 2021-08-24 15:00:30 +08:00
Louis Lam
40d80dfdfd clarify pull request rule 2021-08-24 14:42:35 +08:00
Louis Lam
b9684e32d3 Update CONTRIBUTING.md 2021-08-24 14:06:23 +08:00
LouisLam
8e726da82a eslint: add camelcase rule 2021-08-24 13:58:52 +08:00
Louis Lam
df41c40cc2 add mit license badge in readme 2021-08-24 13:38:21 +08:00
Ponkhy
5dc834794c Row spacing reduced and badge centered 2021-08-24 03:14:33 +02:00
LouisLam
36ace3e56c naming convention and wrap all styles inside .table-shadow-box to avoid unexpected style in the future 2021-08-24 02:02:38 +08:00
LouisLam
3f12afce28 Merge branch 'master' into clean-mobile-table 2021-08-24 01:53:13 +08:00
Louis Lam
833b4733a8 Merge pull request #246 from chakflying/patch-4
Proj: Add npm lint commands
2021-08-24 01:15:49 +08:00
Nelson Chan
066b67dfce Proj: Add lint commands 2021-08-23 23:54:15 +08:00
Ponkhy
b2041cb36b Fixed ESLint Errors 2021-08-23 16:30:11 +02:00
LouisLam
aa2233eb2d log notification error 2021-08-23 20:57:42 +08:00
Ponkhy
e5981b10ce Replaced var with let and removed re-declaration 2021-08-23 13:08:22 +02:00
LouisLam
46cb955172 afterLogin change to non blocking 2021-08-23 18:52:55 +08:00
LouisLam
50f300dd28 heartbeat interval change to use setTimeout() 2021-08-23 18:52:24 +08:00
LouisLam
2f50fc4c00 plan to switch to better-sqlite3, drop node-sqlite3 2021-08-23 17:27:03 +08:00
LouisLam
a02edf1b4f fix detail page empty if the monitor list is not yet ready 2021-08-23 16:59:27 +08:00
LouisLam
a61a65e5ae Merge remote-tracking branch 'origin/master' 2021-08-23 15:50:00 +08:00
Louis Lam
ffaaeae794 Merge pull request #239 from chakflying/fix-ts
Fix: fix typescript errors
2021-08-23 15:49:53 +08:00
Nelson Chan
ee3bf2961c Fix: fix typescript errors 2021-08-23 11:33:24 +08:00
Ponkhy
ce79f8bfc7 CSS optimizations 2021-08-23 01:22:55 +02:00
Ponkhy
c79be19ec3 Added DNS Monitor Type 2021-08-23 00:05:48 +02:00
LouisLam
b892a92fc8 retry if acquire error 2021-08-22 23:35:24 +08:00
Louis Lam
2912ca1248 Merge pull request #237 from Ponkhy/master
Added animation for list page change
2021-08-22 15:11:51 +08:00
LouisLam
2bff1ebe0f update to 1.3.2 2021-08-22 14:44:08 +08:00
LouisLam
ec0dbf3cbe probably still memory leak over time, not sure what happen, change back to singal pool. 2021-08-22 14:43:26 +08:00
LouisLam
210a0d414c fix check update interval too short 2021-08-22 14:07:56 +08:00
Ponkhy
ca38cc91e9 Use bootstrap integraded class instead of new ones 2021-08-22 03:15:25 +02:00
Ponkhy
c90d17bd66 Added animation for list page change 2021-08-21 23:01:43 +02:00
Ponkhy
dbd3f48f68 Added clean monitor table for smaller screens 2021-08-21 21:12:44 +02:00
LouisLam
49ba5fb1b2 update to 1.3.1 2021-08-22 02:13:00 +08:00
LouisLam
d27789a8ae Revert "update to 1.3.1"
This reverts commit b53582a812.
2021-08-22 02:11:40 +08:00
LouisLam
b53582a812 update to 1.3.1 2021-08-22 02:09:16 +08:00
LouisLam
05680472a7 fix high memory usage 2021-08-22 02:07:10 +08:00
LouisLam
ca3b0a0f19 fix setInterval 2021-08-22 00:39:29 +08:00
LouisLam
4571a9b8c1 check update 2021-08-21 19:50:22 +08:00
LouisLam
362eabab8d allow empty block for catch 2021-08-21 19:45:29 +08:00
Louis Lam
a22d0f6951 Merge pull request #231 from ClemontX/master
added K8s-Deployment and edited README
2021-08-21 02:00:33 +08:00
LouisLam
b1168d4cdb update to 1.3.0 2021-08-21 01:44:43 +08:00
Carl Sander
6fcc2253ec changed domain names to example.com 2021-08-20 06:21:59 +00:00
LouisLam
f21937b197 add animation for page change 2021-08-20 02:37:59 +08:00
Louis Lam
1e7623c459 Update README.md 2021-08-19 21:22:16 +08:00
Carl Sander
22047fe932 removed doubled names of ressources 2021-08-19 11:50:05 +00: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
Carl Sander
5b5a32967c fixed README 2021-08-19 09:55:43 +00:00
Carl Sander
ae8b5eea5a Merge branch 'master' into master 2021-08-19 11:50:27 +02: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
Carl Dominik Sander
21640e1bbe changed to tag 1 2021-08-18 17:58:17 +02: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
Carl Dominik Sander
b41799f801 reolved suggestions from @srgvg and @louislam 2021-08-18 16:09:06 +02: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
Carl Sander
60b0ee2959 added K8s-Deployment and edited README 2021-08-18 08:38:05 +00: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
103 changed files with 19165 additions and 8156 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

@@ -16,6 +16,9 @@ module.exports = {
requireConfigFile: false,
},
rules: {
"camelcase": ["warn", {
"properties": "never"
}],
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
"no-unused-vars": "warn",
@@ -36,6 +39,11 @@ module.exports = {
"no-multi-spaces": ["error", {
ignoreEOLComments: true,
}],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"curly": "error",
"object-curly-spacing": ["error", "always"],
"object-curly-newline": "off",
@@ -62,12 +70,15 @@ module.exports = {
exceptAfterSingleLine: true,
}],
"no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"],
//'prefer-template': 'error',
"comma-dangle": ["warn", "only-multiline"],
"no-empty": ["error", {
"allowEmptyCatch": true
}],
"no-control-regex": "off",
"one-var": ["error", "never"],
"max-statements-per-line": ["error", { "max": 1 }]
},
}

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
open_collective: uptime-kuma # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#otechie: # Replace with a single Otechie username
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -6,5 +6,12 @@ labels: help
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Info**
Uptime Kuma Version:
Using Docker?: Yes/No
OS:
Browser:

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**
A clear and concise description of what the bug is.
@@ -20,15 +23,16 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Info**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Error Log**
It is easier for us to find out the problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -6,6 +6,8 @@ labels: enhancement
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -1,3 +1,9 @@
{
"extends": "stylelint-config-recommended",
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"no-descending-specificity": null,
"selector-list-comma-newline-after": null,
"declaration-empty-line-before": null
}
}

1
CNAME Normal file
View File

@@ -0,0 +1 @@
git.kuma.pet

View File

@@ -6,7 +6,38 @@ The project was created with vite.js (vue3). Then I created a sub-directory call
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
# Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
If you are not sure, feel free to create an empty pull request draft first.
## Pull Request Examples
### ✅ High - Medium Priority
- Add a new notification
- Add a chart
- Fix a bug
### *️⃣ Requires one more reviewer
I do not have such knowledge to test it.
- Add k8s supports
### *️⃣ Low Priority
It changed my current workflow and require further studies.
- Change my release approach
### ❌ Won't Merge
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
# Project Styles
@@ -19,16 +50,33 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- All settings in frontend.
- Easy to use
# Coding Styles
- Follow .editorconfig
- Follow eslint
## Name convention
- Javascript/Typescript: camelCaseType
- SQLite: underscore_type
- CSS/SCSS: dash-type
# Tools
- Node.js >= 14
- Git
- IDE that supports .editorconfig (I am using Intellji Idea)
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
- A SQLite tool (I am using SQLite Expert Personal)
# Prepare the dev
# Install dependencies
```bash
npm install
npm install --dev
```
For npm@7, you need --legacy-peer-deps
```
npm install --legacy-peer-deps --dev
```
# Backend Dev
@@ -39,7 +87,6 @@ npm run start-server
# Or
node server/server.js
```
It binds to 0.0.0.0:3001 by default.
@@ -92,7 +139,8 @@ The data and socket logic in "src/mixins/socket.js"
# Database Migration
TODO
1. create `patch{num}.sql` in `./db/`
1. update `latestVersion` in `./server/database.js`
# Unit Test

106
README.md
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>
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
@@ -11,34 +10,36 @@ It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
# Features
## 🥔 Live Demo
* Monitoring uptime for HTTP(s) / TCP / Ping.
Try it!
https://demo.uptime.kuma.pet
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
* 20 seconds interval.
# How to Use
## 🔧 How to Install
## Docker
### 🐳 Docker
```bash
# Create a volume
docker volume create uptime-kuma
# Start the container
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
Browse to http://localhost:3001 after started.
Change Port and Volume
```bash
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
## Without Docker
### 💪🏻 Without Docker
Required Tools: Node.js >= 14, git and pm2.
@@ -48,63 +49,43 @@ cd uptime-kuma
npm run setup
# Option 1. Try it
npm run start-server
node server/server.js
# (Recommended)
# Option 2. Run in background using PM2
# (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
pm2 start server/server.js --name uptime-kuma
```
Browse to http://localhost:3001 after started.
### Advanced Installation
## (Optional) One more step for Reverse Proxy
If you need more options or need to browse via a reserve proxy, please read:
This is optional for someone who want to do reverse proxy.
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket.
Please read wiki for more info:
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
## 🆙 How to Update
## One-click Deploy
Please read:
<!---
Abort. Heroku instance killed the server.js if idle, stupid.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.9)
-->
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
[![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.9 --force
npm install
npm run build
pm2 restart uptime-kuma
```
# What's Next?
## 🆕 What's Next?
I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
# More Screenshots
Project Plan:
https://github.com/louislam/uptime-kuma/projects/1
## 🖼 More Screenshots
Dark Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
Settings Page:
@@ -114,24 +95,21 @@ Telegram Notification Sample:
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
## Motivation
# Motivation
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and unmaintained.
* Want to build a fancy UI.
* Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5.
* Try to use WebSocket with SPA instead of REST API.
* Deploy my first Docker image to Docker Hub.
If you love this project, please consider giving me a ⭐.
# Contribute
## Contribute
If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki.
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.

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"]
}

BIN
db/demo_kuma.db Normal file

Binary file not shown.

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;

10
db/patch7.sql Normal file
View File

@@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD dns_resolve_type VARCHAR(5);
ALTER TABLE monitor
ADD dns_resolve_server VARCHAR(255);
COMMIT;

7
db/patch8.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD dns_last_result VARCHAR(255);
COMMIT;

View File

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

View File

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

View File

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

245
extra/install.batsh Normal file
View File

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

144
extra/simple-dns-server.js Normal file
View File

@@ -0,0 +1,144 @@
/*
* Simple DNS Server
* For testing DNS monitoring type, dev only
*/
const dns2 = require("dns2");
const { Packet } = dns2;
const server = dns2.createServer({
udp: true
});
server.on("request", (request, send, rinfo) => {
for (let question of request.questions) {
console.log(question.name, type(question.type), question.class);
const response = Packet.createResponseFromRequest(request);
if (question.name === "existing.com") {
if (question.type === Packet.TYPE.A) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "1.2.3.4"
});
} if (question.type === Packet.TYPE.AAAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "fe80::::1234:5678:abcd:ef00",
});
} else if (question.type === Packet.TYPE.CNAME) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "cname1.existing.com",
});
} else if (question.type === Packet.TYPE.MX) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
exchange: "mx1.existing.com",
priority: 5
});
} else if (question.type === Packet.TYPE.NS) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
ns: "ns1.existing.com",
});
} else if (question.type === Packet.TYPE.SOA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
primary: "existing.com",
admin: "admin@existing.com",
serial: 2021082701,
refresh: 300,
retry: 3,
expiration: 10,
minimum: 10,
});
} else if (question.type === Packet.TYPE.SRV) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
priority: 5,
weight: 5,
port: 8080,
target: "srv1.existing.com",
});
} else if (question.type === Packet.TYPE.TXT) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
data: "#v=spf1 include:_spf.existing.com ~all",
});
} else if (question.type === Packet.TYPE.CAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
flags: 0,
tag: "issue",
value: "ca.existing.com",
});
}
}
if (question.name === "4.3.2.1.in-addr.arpa") {
if (question.type === Packet.TYPE.PTR) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "ptr1.existing.com",
});
}
}
send(response);
}
});
server.on("listening", () => {
console.log("Listening");
console.log(server.addresses());
});
server.on("close", () => {
console.log("server closed");
});
server.listen({
udp: 5300
});
function type(code) {
for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) {
return name;
}
}
}

View File

@@ -0,0 +1,3 @@
package-lock.json
test.js
languages/

View File

@@ -0,0 +1,78 @@
// Need to use es6 to read language files
import fs from "fs";
import path from "path";
import util from "util";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/**
* Look ma, it's cp -R.
* @param {string} src The path to the thing to copy.
* @param {string} dest The path to the new copy.
*/
const copyRecursiveSync = function (src, dest) {
let exists = fs.existsSync(src);
let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
};
console.log(process.argv)
const baseLangCode = process.argv[2] || "zh-HK";
console.log("Base Lang: " + baseLangCode);
fs.rmdirSync("./languages", { recursive: true });
copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages");
console.log(files);
for (const file of files) {
if (file.endsWith(".js")) {
console.log("Processing " + file);
const lang = await import("./languages/" + file);
let obj;
if (lang.default) {
console.log("is js module");
obj = lang.default;
} else {
console.log("empty file");
obj = {
languageName: "<Your Language name in your language (not in English)>"
};
}
// En first
for (const key in en) {
if (! obj[key]) {
obj[key] = en[key];
}
}
// Base second
for (const key in baseLang) {
if (! obj[key]) {
obj[key] = key;
}
}
const code = "export default " + util.inspect(obj, {
depth: null,
});
fs.writeFileSync(`../../src/languages/${file}`, code);
}
}
fs.rmdirSync("./languages", { recursive: true });
console.log("Done, fix the format by eslint now");

View File

@@ -0,0 +1,12 @@
{
"name": "update-language-files",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

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

@@ -0,0 +1,61 @@
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);
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
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>
<html lang="en">
<head>
<head>
<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="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" />
<title>Uptime Kuma</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

203
install.sh Normal file
View File

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

28
kubernetes/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Uptime-Kuma K8s Deployment
## How does it work?
Kustomize is a tool which builds a complete deployment file for all config elements.
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
## What do i have to edit?
You have to edit the ```ingressroute.yml``` to your needs.
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
- host
- secrets and secret names
- (Cluster)Issuer (optional)
- the Version in the Deployment-File
- update:
- change to newer version and run the above commands, it will update the pods one after another
## How To use:
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
- Edit files mentioned above to your needs
- run ```kustomize build > apply.yml```
- run ```kubectl apply -f apply.yml```
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.

View File

@@ -0,0 +1,10 @@
namespace: uptime-kuma
namePrefix: uptime-kuma-
commonLabels:
app: uptime-kuma
bases:
- uptime-kuma

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
component: uptime-kuma
name: deployment
spec:
selector:
matchLabels:
component: uptime-kuma
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
component: uptime-kuma
spec:
containers:
- name: app
image: louislam/uptime-kuma:1
ports:
- containerPort: 3001
volumeMounts:
- mountPath: /app/data
name: storage
livenessProbe:
exec:
command:
- node
- extra/healthcheck.js
readinessProbe:
httpGet:
path: /
port: 3001
scheme: HTTP
volumes:
- name: storage
persistentVolumeClaim:
claimName: pvc

View File

@@ -0,0 +1,39 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/server-snippets: |
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_cache_bypass $http_upgrade;
}
name: ingress
spec:
tls:
- hosts:
- example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service
port:
number: 3001

View File

@@ -0,0 +1,5 @@
resources:
- deployment.yml
- service.yml
- ingressroute.yml
- pvc.yml

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: service
spec:
selector:
component: uptime-kuma
type: ClusterIP
ports:
- name: http
port: 3001
targetPort: 3001
protocol: TCP

20312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.0.9",
"version": "1.5.3",
"license": "MIT",
"repository": {
"type": "git",
@@ -10,65 +10,83 @@
"node": "14.*"
},
"scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"dev": "vite --host",
"start": "npm run start-server",
"start-server": "node server/server.js",
"update": "",
"build": "vite build",
"vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.9 --target release . --push",
"build-docker": "npm run build-docker-alpine && npm run build-docker-debian",
"build-docker-alpine": "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.5.3 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push",
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.9 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js",
"mark-as-nightly": "node extra/mark-as-nightly.js"
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.2",
"@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
"bootstrap": "^5.0.2",
"bcryptjs": "^2.4.3",
"bootstrap": "^5.1.0",
"chart.js": "^3.5.1",
"chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9",
"compare-versions": "^3.6.0",
"dayjs": "^1.10.6",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2",
"http-graceful-shutdown": "^3.1.4",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3",
"password-hash": "^1.2.2",
"prom-client": "^13.1.0",
"prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.20",
"socket.io": "^4.1.3",
"socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2",
"redbean-node": "0.1.2",
"socket.io": "^4.2.0",
"socket.io-client": "^4.2.0",
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
"tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6",
"vue": "^3.0.5",
"vue": "^3.2.8",
"vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10",
"vue-i18n": "^9.1.7",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.10",
"@types/bootstrap": "^5.0.17",
"@vitejs/plugin-legacy": "^1.5.0",
"@vitejs/plugin-vue": "^1.3.0",
"@vue/compiler-sfc": "^3.1.5",
"core-js": "^3.15.2",
"eslint": "^7.31.0",
"eslint-plugin-vue": "^7.14.0",
"sass": "^1.36.0",
"@babel/eslint-parser": "^7.15.0",
"@types/bootstrap": "^5.1.2",
"@vitejs/plugin-legacy": "^1.5.2",
"@vitejs/plugin-vue": "^1.6.0",
"@vue/compiler-sfc": "^3.2.6",
"core-js": "^3.17.0",
"dns2": "^2.0.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.17.0",
"sass": "^1.38.2",
"stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.3.5",
"vite": "^2.4.4"
"typescript": "^4.4.2",
"vite": "^2.5.3"
}
}

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:

44
server/check-version.js Normal file
View File

@@ -0,0 +1,44 @@
const { setSetting } = require("./util-server");
const axios = require("axios");
const { isDev } = require("../src/util");
exports.version = require("../package.json").version;
exports.latestVersion = null;
let interval;
exports.startInterval = () => {
let check = async () => {
try {
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
if (typeof res.data === "string") {
res.data = JSON.parse(res.data);
}
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
res.data.version = "1000.0.0"
}
exports.latestVersion = res.data.version;
console.log("Latest Version: " + exports.latestVersion);
} catch (_) { }
};
check();
interval = setInterval(check, 3600 * 1000 * 48);
};
exports.enableCheckUpdate = async (value) => {
await setSetting("checkUpdate", value);
clearInterval(interval);
if (value) {
exports.startInterval();
}
};
exports.socket = null;

View File

@@ -1,16 +1,43 @@
const fs = require("fs");
const { sleep } = require("../src/util");
const { R } = require("redbean-node");
const {
setSetting, setting,
} = require("./util-server");
const { setSetting, setting } = require("./util-server");
class Database {
static templatePath = "./db/kuma.db"
static path = "./data/kuma.db";
static latestVersion = 5;
static dataDir;
static path;
static latestVersion = 8;
static noReject = true;
static sqliteInstance = null;
static async connect() {
const acquireConnectionTimeout = 120 * 1000;
R.setup("sqlite", {
filename: Database.path,
useNullAsDefault: true,
acquireConnectionTimeout: acquireConnectionTimeout,
}, {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
});
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");
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
console.log(await R.getAll("PRAGMA journal_mode"));
}
static async patch() {
let version = parseInt(await setting("database_version"));
@@ -24,13 +51,25 @@ class Database {
if (version === this.latestVersion) {
console.info("Database no need to patch");
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = "./data/kuma.db.bak" + version;
const backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
fs.copyFileSync(walPath, walPath + ".bak" + version);
}
// Try catch anything here, if gone wrong, restore the backup
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
@@ -88,32 +127,19 @@ class Database {
}
}
static getBetterSQLite3Database() {
return R.knex.client.acquireConnection();
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught
* @returns {Promise<void>}
*/
static async close() {
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB")
while (true) {
Database.noReject = true;
await R.close()
await sleep(2000)
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db")
}
if (this.sqliteInstance) {
this.sqliteInstance.close();
}
console.log("SQLite closed")
process.removeListener("unhandledRejection", listener);
console.log("Stopped database");
}
}

View File

@@ -6,11 +6,12 @@ dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
const { tcping, ping, checkCertificate } = require("../util-server");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification")
const version = require("../../package.json").version;
/**
* status:
@@ -45,6 +46,11 @@ class Monitor extends BeanModel {
keyword: this.keyword,
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
notificationIDList,
};
}
@@ -65,6 +71,10 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) {
let previousBeat = null;
let retries = 0;
@@ -73,6 +83,10 @@ class Monitor extends BeanModel {
const beat = async () => {
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id,
@@ -99,30 +113,36 @@ class Monitor extends BeanModel {
try {
if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
// Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, {
timeout: this.interval * 1000 * 0.8,
headers: {
"User-Agent": "Uptime-Kuma",
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0,
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
});
bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") {
try {
await this.updateTlsInfo(checkCertificate(res));
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) {
console.error(e.message)
if (e.message !== "No TLS certificate in response") {
console.error(e.message)
}
}
}
@@ -157,6 +177,46 @@ class Monitor extends BeanModel {
bean.ping = await ping(this.hostname);
bean.msg = ""
bean.status = UP;
} else if (this.type === "dns") {
let startTime = dayjs().valueOf();
let dnsMessage = "";
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type == "CAA") {
dnsMessage = dnsRes[0].issue;
} else if (this.dns_resolve_type == "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
} else if (this.dns_resolve_type == "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (this.dns_resolve_type == "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
}
if (this.dnsLastResult !== dnsMessage) {
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
dnsMessage,
this.id
]);
}
bean.msg = dnsMessage;
bean.status = UP;
}
if (this.isUpsideDown()) {
@@ -223,7 +283,8 @@ class Monitor extends BeanModel {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
}
@@ -240,22 +301,22 @@ class Monitor extends BeanModel {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
}
prometheus.update(bean)
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)
Monitor.sendStats(io, this.id, this.user_id)
await R.store(bean);
prometheus.update(bean, tlsInfo);
previousBeat = bean;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
}
beat();
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
}
stop() {
clearInterval(this.heartbeatInterval)
clearTimeout(this.heartbeatInterval);
}
/**
@@ -275,7 +336,7 @@ class Monitor extends BeanModel {
/**
* Store TLS info to database
* @param checkCertificateResult
* @returns {Promise<void>}
* @returns {Promise<object>}
*/
async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@@ -287,13 +348,21 @@ class Monitor extends BeanModel {
}
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean);
return checkCertificateResult;
}
static async sendStats(io, monitorID, userID) {
Monitor.sendAvgPing(24, io, monitorID, userID);
Monitor.sendUptime(24, io, monitorID, userID);
Monitor.sendUptime(24 * 30, io, monitorID, userID);
Monitor.sendCertInfo(io, monitorID, userID);
const hasClients = getTotalClientInRoom(io, userID) > 0;
if (hasClients) {
await Monitor.sendAvgPing(24, io, monitorID, userID);
await Monitor.sendUptime(24, io, monitorID, userID);
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
await Monitor.sendCertInfo(io, monitorID, userID);
} else {
debug("No clients in the room, no need to send stats");
}
}
/**
@@ -301,6 +370,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let avgPing = parseInt(await R.getCell(`
SELECT AVG(ping)
FROM heartbeat
@@ -311,6 +382,8 @@ class Monitor extends BeanModel {
monitorID,
]));
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
io.to(userID).emit("avgPing", monitorID, avgPing);
}
@@ -330,6 +403,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let sec = duration * 3600;
let heartbeatList = await R.getAll(`
@@ -341,6 +416,8 @@ class Monitor extends BeanModel {
monitorID,
]);
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
let downtime = 0;
let total = 0;
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,91 @@ class Notification {
} else if (notification.type === "discord") {
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let data = {
username: "Uptime-Kuma",
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, data)
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
let url;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == 0) {
var alertColor = "16711680";
let discorddowndata = {
username: discordDisplayName,
embeds: [{
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
color: 16711680,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: 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) {
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: url.startsWith("http") ? "[Visit Service](" + url + ")" : 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) {
throwGeneralAxiosError(error)
}
@@ -155,6 +206,34 @@ class Notification {
console.log(error)
return false;
}
} else if (notification.type === "octopush") {
try {
let config = {
headers: {
"api-key": notification.octopushAPIKey,
"api-login": notification.octopushLogin,
"cache-control": "no-cache"
}
};
let data = {
"recipients": [
{
"phone_number": notification.octopushPhoneNumber
}
],
//octopush not supporting non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": notification.octopushSMSType,
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "slack") {
try {
if (heartbeatJSON == null) {
@@ -213,6 +292,150 @@ class Notification {
throwGeneralAxiosError(error)
}
} else if (notification.type === "rocket.chat") {
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Rocket.chat testing successful.",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
"attachments": [
{
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
"title_link": notification.rocketbutton,
"text": "*Message*\n" + msg,
"color": "#32cd32"
}
]
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "mattermost") {
try {
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let mattermostTestData = {
username: mattermostUserName,
text: msg,
}
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
return okMsg;
}
const mattermostChannel = notification.mattermostchannel;
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
if (heartbeatJSON["status"] == 0) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went down.",
color: "#FF0000",
title:
"❌ " +
monitorJSON["name"] +
" service went down. ❌",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Error",
value: heartbeatJSON["msg"],
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] == 1) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went up!",
color: "#32CD32",
title:
"✅ " +
monitorJSON["name"] +
" service went up! ✅",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostupdata
);
return okMsg;
}
} catch (error) {
throwGeneralAxiosError(error);
}
} else if (notification.type === "pushover") {
let pushoverlink = "https://api.pushover.net/1/messages.json"
try {
@@ -288,6 +511,88 @@ class Notification {
throwGeneralAxiosError(error)
}
} else if (notification.type === "pushbullet") {
try {
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
let config = {
headers: {
"Access-Token": notification.pushbulletAccessToken,
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
let testdata = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == 0) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == 1) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "line") {
try {
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == 0) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == 1) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else {
throw new Error("Notification type is not supported")
}
@@ -331,15 +636,21 @@ class Notification {
static async smtp(notification, msg) {
let transporter = nodemailer.createTransport({
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
auth: {
};
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
},
});
};
}
let transporter = nodemailer.createTransport(config);
// send mail with defined transport object
await transporter.sendMail({

View File

@@ -1,5 +1,5 @@
const passwordHashOld = require("password-hash");
const bcrypt = require("bcrypt");
const bcrypt = require("bcryptjs");
const saltRounds = 10;
exports.generate = function (password) {

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
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")
const fs = require("fs");
const http = require("http");
const https = require("https");
console.log("Importing 3rd-party libraries")
debug("Importing express");
const express = require("express");
debug("Importing socket.io");
const { Server } = require("socket.io");
debug("Importing dayjs");
const dayjs = require("dayjs");
debug("Importing redbean-node");
const { R } = require("redbean-node");
debug("Importing jsonwebtoken");
@@ -26,7 +26,7 @@ console.log("Importing this project modules");
debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
debug("Importing Notification");
const { Notification } = require("./notification");
debug("Importing Database");
@@ -38,15 +38,49 @@ const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv);
const version = require("../package.json").version;
const hostname = process.env.HOST || args.host || "0.0.0.0"
const checkVersion = require("./check-version");
console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
const hostname = process.env.HOST || args.host;
const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version)
// SSL
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
// Demo Mode?
const demoMode = args["demo"] || false;
if (demoMode) {
console.log("==== Demo Mode ====");
}
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
console.log(`Data Dir: ${Database.dataDir}`);
console.log("Creating express and socket.io instance")
const app = express();
const server = http.createServer(app);
let server;
if (sslKey && sslCert) {
console.log("Server Type: HTTPS");
server = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, app);
} else {
console.log("Server Type: HTTP");
server = http.createServer(app);
}
const io = new Server(server);
app.use(express.json())
@@ -87,24 +121,35 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
// 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
// Prometheus API metrics /metrics
// 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
app.get("*", function(request, response, next) {
response.end(indexHTML)
app.get("*", async (_request, response) => {
response.send(indexHTML);
});
console.log("Adding socket handler")
io.on("connection", async (socket) => {
socket.emit("info", {
version,
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
})
totalClient++;
@@ -136,7 +181,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user) {
debug("afterLogin")
await afterLogin(socket, user)
afterLogin(socket, user)
debug("afterLogin ok")
@@ -164,7 +209,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let user = await login(data.username, data.password)
if (user) {
await afterLogin(socket, user)
afterLogin(socket, user)
callback({
ok: true,
@@ -230,6 +275,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor)
bean.user_id = socket.userID
await R.store(bean)
@@ -274,6 +322,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
await R.store(bean)
@@ -408,10 +460,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password.newPassword),
socket.userID,
]);
user.resetPassword(password.newPassword);
callback({
ok: true,
@@ -538,10 +587,14 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
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"))
afterLogin(socket, await R.findOne("user"))
socket.emit("autoLogin")
} else {
debug("need auth")
@@ -549,16 +602,27 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
});
console.log("Init")
console.log("Init the server")
server.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
await Database.close();
});
server.listen(port, hostname, () => {
console.log(`Listening on ${hostname}:${port}`);
if (hostname) {
console.log(`Listening on ${hostname}:${port}`);
} else {
console.log(`Listening on ${port}`);
}
startMonitors();
checkVersion.startInterval();
});
})();
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,
])
@@ -590,6 +654,8 @@ async function sendMonitorList(socket) {
}
async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID,
@@ -600,6 +666,9 @@ async function sendNotificationList(socket) {
}
io.to(socket.userID).emit("notificationList", result)
timeLogger.print("Send Notification List");
return list;
}
@@ -608,20 +677,27 @@ async function afterLogin(socket, user) {
socket.join(user.id)
let monitorList = await sendMonitorList(socket)
sendNotificationList(socket)
await sleep(500);
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
await sendHeartbeatList(socket, monitorID);
}
sendNotificationList(socket)
for (let monitorID in monitorList) {
await sendImportantHeartbeatList(socket, monitorID);
}
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id)
}
}
async function getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
])
@@ -645,32 +721,22 @@ async function initDatabase() {
}
console.log("Connecting to Database")
R.setup("sqlite", {
filename: Database.path,
});
await Database.connect();
console.log("Connected")
// Patch the database
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` = ? ", [
"jwtSecret",
]);
if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.")
jwtSecretBean = R.dispense("setting")
jwtSecretBean.key = "jwtSecret"
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
await R.store(jwtSecretBean)
console.log("Stored JWT secret into database")
console.log("JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret();
console.log("Stored JWT secret into database");
} else {
console.log("Load JWT secret from database.")
console.log("Load JWT secret from database.");
}
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
@@ -730,15 +796,22 @@ async function startMonitors() {
let list = await R.find("monitor", " active = 1 ")
for (let monitor of list) {
monitor.start(io)
monitorList[monitor.id] = monitor;
}
for (let monitor of list) {
monitor.start(io);
// Give some delays, so all monitors won't make request at the same moment when just start the server.
await sleep(getRandomInt(300, 1000));
}
}
/**
* Send Heartbeat History list to socket
*/
async function sendHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
ORDER BY time DESC
@@ -754,9 +827,13 @@ async function sendHeartbeatList(socket, monitorID) {
}
socket.emit("heartbeatList", monitorID, result)
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`)
}
async function sendImportantHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
@@ -766,6 +843,8 @@ async function sendImportantHeartbeatList(socket, monitorID) {
monitorID,
])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
socket.emit("importantHeartbeatList", monitorID, list)
}
@@ -780,11 +859,10 @@ async function shutdownFunction(signal) {
}
await sleep(2000);
await Database.close();
console.log("Stopped DB")
}
function finalFunction() {
console.log("Graceful Shutdown Done")
console.log("Graceful shutdown successfully!");
}
gracefulShutdown(server, {
@@ -795,3 +873,9 @@ gracefulShutdown(server, {
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction, // finally function (sync) - e.g. for logging
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
console.trace(error);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});

View File

@@ -2,6 +2,28 @@ const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const { R } = require("redbean-node");
const { debug } = require("../src/util");
const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
const { Resolver } = require("dns");
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
*/
exports.initJWTSecret = async () => {
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);
if (! jwtSecretBean) {
jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret";
}
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
await R.store(jwtSecretBean);
return jwtSecretBean;
}
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
@@ -9,7 +31,7 @@ exports.tcping = function (hostname, port) {
address: hostname,
port: port,
attempts: 1,
}, function(err, data) {
}, function (err, data) {
if (err) {
reject(err);
@@ -24,15 +46,30 @@ exports.tcping = function (hostname, port) {
});
}
exports.ping = function (hostname) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname);
exports.ping = async (hostname) => {
try {
return await exports.pingAsync(hostname);
} catch (e) {
// If the host cannot be resolved, try again with ipv6
if (e.message.includes("service not known")) {
return await exports.pingAsync(hostname, true);
} else {
throw e;
}
}
}
ping.send(function(err, ms) {
exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
ipv6
});
ping.send(function (err, ms, stdout) {
if (err) {
reject(err)
reject(err);
} else if (ms === null) {
reject(new Error("timeout"))
reject(new Error(stdout))
} else {
resolve(Math.round(ms))
}
@@ -40,6 +77,30 @@ exports.ping = function (hostname) {
});
}
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
resolver.setServers([resolver_server]);
return new Promise((resolve, reject) => {
if (rrtype == "PTR") {
resolver.reverse(hostname, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
} else {
resolver.resolve(hostname, rrtype, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
}
})
}
exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key,
@@ -58,7 +119,7 @@ exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
])
if (! bean) {
if (!bean) {
bean = R.dispense("setting")
bean.key = key;
}
@@ -158,3 +219,55 @@ exports.checkCertificate = function (res) {
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;
}
exports.getTotalClientInRoom = (io, roomName) => {
const sockets = io.sockets;
if (! sockets) {
return 0;
}
const adapter = sockets.adapter;
if (! adapter) {
return 0;
}
const room = adapter.rooms.get(roomName);
if (room) {
return room.size;
} else {
return 0;
}
}

View File

@@ -2,12 +2,48 @@
@import "node_modules/bootstrap/scss/bootstrap";
#app {
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
}
h1 {
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, 0.1);
.dark & {
box-shadow: 0 15px 70px rgb(0 0 0);
background-color: $dark-bg;
}
}
.VuePagination__count {
font-size: 13px;
text-align: center;
}
.shadow-box {
overflow: hidden;
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
//overflow: hidden; // Forget why add this, but multiple select hide by this
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
padding: 10px;
border-radius: 10px;
@@ -29,10 +65,226 @@
background-color: $highlight;
border-color: $highlight;
}
.dark & {
color: $dark-font-color2;
}
}
.modal-content {
border-radius: 1rem;
backdrop-filter: blur(3px);
.btn-warning {
color: white;
&:hover, &:active, &:focus, &.active {
color: white;
}
}
.btn-info {
color: white;
&:hover, &:active, &:focus, &.active {
color: white;
}
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
thead {
display: none;
}
tbody {
.shadow-box {
background-color: white;
}
}
tr {
margin-top: 0 !important;
padding: 4px 10px !important;
display: block;
margin-bottom: 6px;
td:first-child {
font-weight: bold;
}
td:nth-child(-n+3) {
text-align: center;
}
td:last-child {
text-align: left;
}
td {
border-bottom: 1px solid $dark-font-color;
display: block;
padding: 4px;
.badge {
margin: auto;
display: block;
width: 30%;
}
}
}
}
}
// Dark Theme override here
.dark {
background-color: #090c10;
color: $dark-font-color;
&::-webkit-scrollbar-thumb, ::-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;
&.btn-info {
color: white;
}
}
.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-warning {
color: $dark-font-color2;
&:hover, &:active, &:focus, &.active {
color: $dark-font-color2;
}
}
.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;
}
@media (max-width: 550px) {
.table-shadow-box {
tbody {
.shadow-box {
background-color: $dark-bg2;
td {
border-bottom: 1px solid $dark-border-color;
}
}
}
}
}
}
/*
* Transitions
*/
// page-change
.slide-fade-enter-active {
transition: all 0.2s $easing-in;
}
.slide-fade-leave-active {
transition: all 0.2s $easing-in;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(50px);
opacity: 0;
}

View File

@@ -1,8 +1,18 @@
$primary: #5CDD8B;
$danger: #DC3545;
$primary: #5cdd8b;
$danger: #dc3545;
$warning: #f8a306;
$link-color: #111;
$border-radius: 50rem;
$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

@@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
Confirm
{{ $t("Confirm") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
@@ -35,7 +35,7 @@ export default {
},
yesText: {
type: String,
default: "Yes",
default: "Yes", // TODO: No idea what to translate this
},
noText: {
type: String,

View File

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

View File

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

View File

@@ -6,12 +6,12 @@
<div class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">Username</label>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">Password</label>
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
@@ -19,12 +19,12 @@
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember">
Remember me
{{ $t("Remember me") }}
</label>
</div>
</div>
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
Login
{{ $t("Login") }}
</button>
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">

View File

@@ -0,0 +1,143 @@
<template>
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<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,
},
props: {
scrollbar: {
type: Boolean,
},
},
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 {
&.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto;
position: sticky;
top: 10px;
}
.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

@@ -5,30 +5,35 @@
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
Setup Notification
{{ $t("Setup Notification") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<label for="type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option>
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
<option value="discord">Discord</option>
<option value="signal">Signal</option>
<option value="gotify">Gotify</option>
<option value="slack">Slack</option>
<option value="rocket.chat">Rocket.chat</option>
<option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
<option value="line">Line Messenger</option>
<option value="mattermost">Mattermost</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="notification.name" type="text" class="form-control" required>
</div>
@@ -147,6 +152,11 @@
You can get this by going to Server Settings -> Integrations -> Create Webhook
</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 v-if="notification.type === 'signal'">
@@ -202,7 +212,7 @@
<template v-if="notification.type === 'slack'">
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label>
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
@@ -213,7 +223,7 @@
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
@@ -230,28 +240,122 @@
</div>
</template>
<template v-if="notification.type === 'rocket.chat'">
<div class="mb-3">
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
<label for="rocket-username" class="form-label">Username</label>
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
<label for="rocket-channel" class="form-label">Channel Name</label>
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'mattermost'">
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">Username</label>
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
<label for="mattermost-channel" class="form-label">Channel Name</label>
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</p>
<p style="margin-top: 8px;">
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input type="text" class="form-control" id="pushy-app-token" required v-model="notification.pushyAPIKey">
<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 type="text" class="form-control" id="pushy-user-key" required v-model="notification.pushyToken">
<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>
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</p>
</template>
<template v-if="notification.type === 'octopush'">
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<input id="octopush-key" v-model="notification.octopushAPIKey" type="text" class="form-control" required>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">SMS Type</label>
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
</select>
<div class="form-text">
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
</div>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</p>
</template>
<template v-if="notification.type === 'pushover'">
<div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
<label for="pushover-device" class="form-label">Device</label>
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
@@ -291,7 +395,7 @@
<option>none</option>
</select>
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p>
@@ -320,31 +424,61 @@
<p>
Status:
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise">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>
</div>
</template>
<template v-if="notification.type === 'lunasea'">
<div class="mb-3">
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color:red;"><sup>*</sup></span>Required</p>
<p><span style="color: red;"><sup>*</sup></span>Required</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushbullet'">
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">Access Token</label>
<input id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" type="text" class="form-control" required>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</p>
</template>
<template v-if="notification.type === 'line'">
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">Channel access token</label>
<input id="line-channel-access-token" v-model="notification.lineChannelAccessToken" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Basic Settings</b>
</div>
<div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label>
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Messaging API</b>
</div>
<div class="form-text" style="margin-top: 8px;">
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
</div>
</template>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
Delete
{{ $t("Delete") }}
</button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
Test
{{ $t("Test") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
Save
{{ $t("Save") }}
</button>
</div>
</div>
@@ -352,8 +486,8 @@
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
Are you sure want to delete this notification for all monitors?
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteNotification">
{{ $t("deleteNotificationMsg") }}
</Confirm>
</template>
@@ -508,3 +642,13 @@ export default {
},
}
</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,176 @@
<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 {
// Configurable filtering on top of the returned data
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: {
// Hide points on chart unless mouse-over
radius: 0,
hitRadius: 100,
},
},
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,
},
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
offset: false,
},
},
y: {
title: {
display: true,
text: this.$t("respTime"),
},
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,
backgroundColor: this.$root.theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)",
bodyColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
titleColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
filter: function (tooltipItem) {
return tooltipItem.datasetIndex === 0; // Hide tooltip on Bar Chart
},
callbacks: {
label: (context) => {
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
},
}
},
legend: {
display: false,
},
},
}
},
chartData() {
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
if (this.monitorId in this.$root.heartbeatList) {
this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
const x = this.$root.datetime(beat.time);
pingData.push({
x,
y: beat.ping,
});
downData.push({
x,
y: beat.status === 0 ? 1 : 0,
})
});
}
return {
datasets: [
{
// Line Chart
data: pingData,
fill: "origin",
tension: 0.2,
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
},
{
// Bar Chart
type: "bar",
data: downData,
borderColor: "#00000000",
backgroundColor: "#DC354568",
yAxisID: "y1",
barThickness: "flex",
barPercentage: 1,
categoryPercentage: 1,
},
],
};
},
},
};
</script>

View File

@@ -27,18 +27,18 @@ export default {
text() {
if (this.status === 0) {
return "Down"
return this.$t("Down");
}
if (this.status === 1) {
return "Up"
return this.$t("Up");
}
if (this.status === 2) {
return "Pending"
return this.$t("Pending");
}
return "Unknown"
return this.$t("Unknown");
},
},
}

View File

@@ -22,7 +22,7 @@ export default {
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
}
return "N/A"
return this.$t("notAvailableShort")
},
color() {

View File

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

18
src/languages/README.md Normal file
View File

@@ -0,0 +1,18 @@
# How to translate
1. Fork this repo.
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
3. `npm run update-language-files --base-lang=de-DE`
6. Your language file should be filled in. You can translate now.
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
8. Import your language file in `src/main.js` and add it to `languageList` constant.
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
One of good examples:
https://github.com/louislam/uptime-kuma/pull/316/files
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏

112
src/languages/da-DK.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Danish",
Settings: "Indstillinger",
Dashboard: "Dashboard",
"New Update": "Opdatering tilgængelig",
Language: "Sprog",
Appearance: "Udseende",
Theme: "Tema",
General: "Generelt",
Version: "Version",
"Check Update On GitHub": "Tjek efter opdateringer på Github",
List: "Liste",
Add: "Tilføj",
"Add New Monitor": "Tilføj ny Overvåger",
"Quick Stats": "Oversigt",
Up: "Aktiv",
Down: "Inaktiv",
Pending: "Afventer",
Unknown: "Ukendt",
Pause: "Pause",
pauseDashboardHome: "Pauset",
Name: "Navn",
Status: "Status",
DateTime: "Dato / Tid",
Message: "Beskeder",
"No important events": "Inden vigtige begivenheder",
Resume: "Fortsæt",
Edit: "Rediger",
Delete: "Slet",
Current: "Aktuelt",
Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb",
days: "Dage",
day: "Dag",
"-day": "-Dage",
hour: "Timer",
"-hour": "-Timer",
checkEverySecond: "Tjek hvert {0} sekund",
"Avg.": "Gennemsnit",
Response: " Respons",
Ping: "Ping",
"Monitor Type": "Overvåger Type",
Keyword: "Nøgleord",
"Friendly Name": "Visningsnavn",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Taktinterval",
Retries: "Gentagelser",
retriesDescription: "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.",
Advanced: "Avanceret",
ignoreTLSError: "Ignorere TLS/SSL web fejl",
"Upside Down Mode": "Omvendt tilstand",
upsideDownModeDescription: "Håndter tilstanden omvendt. Hvis tjenesten er tilgængelig, vises den som inaktiv.",
"Max. Redirects": "Maks. Omdirigeringer",
maxRedirectDescription: "Maksimalt antal omdirigeringer, der skal følges. Indstil til 0 for at deaktivere omdirigeringer.",
"Accepted Status Codes": "Tilladte HTTP-Statuskoder",
acceptedStatusCodesDescription: "Vælg de statuskoder, der stadig skal vurderes som vellykkede.",
Save: "Gem",
Notifications: "Underretninger",
"Not available, please setup.": "Ikke tilgængelige, opsæt venligst.",
"Setup Notification": "Opsæt underretninger",
Light: "Lys",
Dark: "Mørk",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Tidslinje",
Normal: "Normal",
Bottom: "Bunden",
None: "Ingen",
Timezone: "Tidszone",
"Search Engine Visibility": "Søgemaskine synlighed",
"Allow indexing": "Tillad indeksering",
"Discourage search engines from indexing site": "Frabed søgemaskiner at indeksere webstedet",
"Change Password": "Ændre adgangskode",
"Current Password": "Nuværende adgangskode",
"New Password": "Ny adgangskode",
"Repeat New Password": "Gentag den nye adgangskode",
passwordNotMatchMsg: "Adgangskoderne er ikke ens.",
"Update Password": "Opdater adgangskode",
"Disable Auth": "Deaktiver autentificering",
"Enable Auth": "Aktiver autentificering",
Logout: "Log ud",
notificationDescription: "Tildel underretninger til Overvåger(e), så denne funktion træder i kraft.",
Leave: "Verlassen",
"I understand, please disable": "Jeg er indforstået, deaktiver venligst",
Confirm: "Bekræft",
Yes: "Ja",
No: "Nej",
Username: "Brugernavn",
Password: "Adgangskode",
"Remember me": "Husk mig",
Login: "Log ind",
"No Monitors, please": "Ingen Overvågere",
"add one": "tilføj en",
"Notification Type": "Underretningstype",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Certifikatoplysninger",
keywordDescription: "Søg efter et søgeord i almindelig HTML- eller JSON -output. Bemærk, at der skelnes mellem store og små bogstaver.",
deleteMonitorMsg: "Er du sikker på, at du vil slette overvågeren?",
deleteNotificationMsg: "Er du sikker på, at du vil slette denne underretning for alle overvågere? ",
resoverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.",
"Resolver Server": "Navne-server",
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
"Last Result": "Seneste resultat",
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
"Create your admin account": "Opret din administratorkonto",
"Repeat Password": "Gentag adgangskoden",
"Resource Record Type": "Resource Record Type",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/de-DE.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "German",
Settings: "Einstellungen",
Dashboard: "Dashboard",
"New Update": "Update Verfügbar",
Language: "Sprache",
Appearance: "Erscheinung",
Theme: "Thema",
General: "Allgemein",
Version: "Version",
"Check Update On GitHub": "Überprüfen von Updates auf Github",
List: "Liste",
Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor",
"Quick Stats": "Übersicht",
Up: "Aktiv",
Down: "Inaktiv",
Pending: "Ausstehend",
Unknown: "Unbekannt",
Pause: "Pausieren",
pauseDashboardHome: "Pausiert",
Name: "Name",
Status: "Status",
DateTime: "Datum / Uhrzeit",
Message: "Nachricht",
"No important events": "Keine wichtigen Ereignisse",
Resume: "Fortsetzen",
Edit: "Bearbeiten",
Delete: "Löschen",
Current: "Aktuell",
Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf",
days: "Tage",
day: "Tag",
"-day": "-Tage",
hour: "Stunde",
"-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden",
"Avg.": "Durchschn. ",
Response: " Antwortzeit",
Ping: "Ping",
"Monitor Type": "Monitor Typ",
Keyword: "Schlüsselwort",
"Friendly Name": "Anzeigename",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Taktintervall",
Retries: "Wiederholungen",
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten",
"Upside Down Mode": "Umgedrehter Modus",
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.",
"Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
Save: "Speichern",
Notifications: "Benachrichtigungen",
"Not available, please setup.": "Keine verfügbar, bitte einrichten.",
"Setup Notification": "Benachrichtigung einrichten",
Light: "Hell",
Dark: "Dunkel",
Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Taktleiste",
Normal: "Normal",
Bottom: "Unten",
None: "Keine",
Timezone: "Zeitzone",
"Search Engine Visibility": "Suchmaschinensichtbarkeit",
"Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
"Change Password": "Passwort ändern",
"Current Password": "Dezeitiges Passwort",
"New Password": "Neues Passwort",
"Repeat New Password": "Wiederhole neues Passwort",
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
"Update Password": "Ändere Passwort",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
Logout: "Ausloggen",
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätige",
Yes: "Ja",
No: "Nein",
Username: "Benutzername",
Password: "Passwort",
"Remember me": "Passwort merken",
Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen",
"Notification Type": "Benachrichtigungs Dienst",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Zertifikatsinfo",
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
"Resolver Server": "Auflösungsserver",
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis",
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
"Create your admin account": "Erstelle dein Admin Konto",
"Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type",
respTime: "Antw. Zeit (ms)",
notAvailableShort: "N/A"
}

112
src/languages/en.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "English",
checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg. ",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
passwordNotMatchMsg: "The repeat password does not match.",
notificationDescription: "Please assign a notification to monitor(s) to get it to work.",
keywordDescription: "Search keyword in plain html or JSON response and it is case-sensitive",
pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.",
rrtypeDescription: "Select the RR-Type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?",
Settings: "Settings",
Dashboard: "Dashboard",
"New Update": "New Update",
Language: "Language",
Appearance: "Appearance",
Theme: "Theme",
General: "General",
Version: "Version",
"Check Update On GitHub": "Check Update On GitHub",
List: "List",
Add: "Add",
"Add New Monitor": "Add New Monitor",
"Quick Stats": "Quick Stats",
Up: "Up",
Down: "Down",
Pending: "Pending",
Unknown: "Unknown",
Pause: "Pause",
Name: "Name",
Status: "Status",
DateTime: "DateTime",
Message: "Message",
"No important events": "No important events",
Resume: "Resume",
Edit: "Edit",
Delete: "Delete",
Current: "Current",
Uptime: "Uptime",
"Cert Exp.": "Cert Exp.",
days: "days",
day: "day",
"-day": "-day",
hour: "hour",
"-hour": "-hour",
Response: "Response",
Ping: "Ping",
"Monitor Type": "Monitor Type",
Keyword: "Keyword",
"Friendly Name": "Friendly Name",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Heartbeat Interval",
Retries: "Retries",
Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects",
"Accepted Status Codes": "Accepted Status Codes",
Save: "Save",
Notifications: "Notifications",
"Not available, please setup.": "Not available, please setup.",
"Setup Notification": "Setup Notification",
Light: "Light",
Dark: "Dark",
Auto: "Auto",
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
Normal: "Normal",
Bottom: "Bottom",
None: "None",
Timezone: "Timezone",
"Search Engine Visibility": "Search Engine Visibility",
"Allow indexing": "Allow indexing",
"Discourage search engines from indexing site": "Discourage search engines from indexing site",
"Change Password": "Change Password",
"Current Password": "Current Password",
"New Password": "New Password",
"Repeat New Password": "Repeat New Password",
"Update Password": "Update Password",
"Disable Auth": "Disable Auth",
"Enable Auth": "Enable Auth",
Logout: "Logout",
Leave: "Leave",
"I understand, please disable": "I understand, please disable",
Confirm: "Confirm",
Yes: "Yes",
No: "No",
Username: "Username",
Password: "Password",
"Remember me": "Remember me",
Login: "Login",
"No Monitors, please": "No Monitors, please",
"add one": "add one",
"Notification Type": "Notification Type",
Email: "Email",
Test: "Test",
"Certificate Info": "Certificate Info",
"Resolver Server": "Resolver Server",
"Resource Record Type": "Resource Record Type",
"Last Result": "Last Result",
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/es-ES.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Español",
checkEverySecond: "Comprobar cada {0} segundos.",
"Avg.": "Media. ",
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.",
acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.",
passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
pauseDashboardHome: "Pausar",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
rrtypeDescription: "Selecciona el tipo de registro que quieres monitorizar",
pauseMonitorMsg: "¿Seguro que quieres pausar?",
Settings: "Ajustes",
Dashboard: "Panel",
"New Update": "Vueva actualización",
Language: "Idioma",
Appearance: "Apariencia",
Theme: "Tema",
General: "General",
Version: "Versión",
"Check Update On GitHub": "Comprobar actualizaciones en GitHub",
List: "Lista",
Add: "Añadir",
"Add New Monitor": "Añadir nuevo monitor",
"Quick Stats": "Estadísticas rápidas",
Up: "Funcional",
Down: "Caído",
Pending: "Pendiente",
Unknown: "Desconociso",
Pause: "Pausa",
Name: "Nombre",
Status: "Estado",
DateTime: "Fecha y Hora",
Message: "Mensaje",
"No important events": "No hay eventos importantes",
Resume: "Reanudar",
Edit: "Editar",
Delete: "Eliminar",
Current: "Actual",
Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.",
days: "días",
day: "día",
"-day": "-día",
hour: "hora",
"-hour": "-hora",
Response: "Respuesta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
Keyword: "Palabra clave",
"Friendly Name": "Nombre sencillo",
URL: "URL",
Hostname: "Nombre del host",
Port: "Puerto",
"Heartbeat Interval": "Intervalo de latido",
Retries: "Reintentos",
Advanced: "Avanzado",
"Upside Down Mode": "Modo invertido",
"Max. Redirects": "Máx. redirecciones",
"Accepted Status Codes": "Códigos de estado aceptados",
Save: "Guardar",
Notifications: "Notificaciones",
"Not available, please setup.": "No disponible, por favor configurar.",
"Setup Notification": "Configurar notificación",
Light: "Claro",
Dark: "Oscuro",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Barra de intervalo de latido",
Normal: "Normal",
Bottom: "Abajo",
None: "Ninguno",
Timezone: "Zona horaria",
"Search Engine Visibility": "Visibilidad motor de búsqueda",
"Allow indexing": "Permitir indexación",
"Discourage search engines from indexing site": "Disuadir a los motores de búsqueda de indexar el sitio",
"Change Password": "Cambiar contraseña",
"Current Password": "Contraseña actual",
"New Password": "Nueva contraseña",
"Repeat New Password": "Repetir nueva contraseña",
"Update Password": "Actualizar contraseña",
"Disable Auth": "Deshabilitar Autenticación ",
"Enable Auth": "Habilitar Autenticación ",
Logout: "Cerrar sesión",
Leave: "Salir",
"I understand, please disable": "Lo comprendo, por favor deshabilitar",
Confirm: "Confirmar",
Yes: "Sí",
No: "No",
Username: "Usuario",
Password: "Contraseña",
"Remember me": "Recordarme",
Login: "Acceso",
"No Monitors, please": "Sin monitores, por favor",
"add one": "añade uno",
"Notification Type": "Tipo de notificación",
Email: "Email",
Test: "Test",
"Certificate Info": "Información del certificado ",
"Resolver Server": "Servidor de resolución",
"Resource Record Type": "Tipo de Registro",
"Last Result": "Último resultado",
"Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A"
}

112
src/languages/fr.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Français (France)",
Settings: "Paramètres",
Dashboard: "Tableau de bord",
"New Update": "Mise à jour disponible",
Language: "Langue",
Appearance: "Apparence",
Theme: "Thème",
General: "Général",
Version: "Version",
"Check Update On GitHub": "Consulter les mises à jour sur Github",
List: "Lister",
Add: "Ajouter",
"Add New Monitor": "Ajouter une nouvelle sonde",
"Quick Stats": "Résumé",
Up: "En ligne",
Down: "Hors ligne",
Pending: "En attente",
Unknown: "Inconnu",
Pause: "En Pause",
pauseDashboardHome: "Éléments mis en pause",
Name: "Nom",
Status: "État",
DateTime: "Heure",
Message: "Messages",
"No important events": "Pas d'évènements important",
Resume: "Reprendre",
Edit: "Modifier",
Delete: "Supprimer",
Current: "Actuellement",
Uptime: "Uptime",
"Cert Exp.": "Certificat expiré",
days: "Jours",
day: "Jour",
"-day": "Journée",
hour: "Heure",
"-hour": "Heures",
checkEverySecond: "Vérifier toutes les {0} secondes",
"Avg.": "Moyen",
Response: "Temps de réponse",
Ping: "Ping",
"Monitor Type": "Type de Sonde",
Keyword: "Mot-clé",
"Friendly Name": "Nom d'affichage",
URL: "URL",
Hostname: "Nom d'hôte",
Port: "Port",
"Heartbeat Interval": "Intervale de vérification",
Retries: "Essais",
retriesDescription: "Nombre d'essais avant que le service soit déclaré hors-ligne.",
Advanced: "Avancé",
ignoreTLSError: "Ignorer les erreurs liées au certificat SSL/TLS",
"Upside Down Mode": "Mode inversé",
upsideDownModeDescription: "Si le service est en ligne, il sera alors noté hors-ligne et vice-versa.",
"Max. Redirects": "Nombre maximum de redirections",
maxRedirectDescription: "Nombre maximal de redirections avant que le service soit noté hors-ligne.",
"Accepted Status Codes": "Codes HTTP",
acceptedStatusCodesDescription: "Codes HTTP considérés comme en ligne",
Save: "Sauvegarder",
Notifications: "Notifications",
"Not available, please setup.": "Pas de système de notification disponible, merci de le configurer",
"Setup Notification": "Créer une notification",
Light: "Clair",
Dark: "Sombre",
Auto: "Automatique",
"Theme - Heartbeat Bar": "Voir les services surveillés",
Normal: "Général",
Bottom: "En dessous",
None: "Non",
Timezone: "Fuseau Horaire",
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
"Discourage search engines from indexing site": "Refuser l'indexation par des moteurs de recherche",
"Change Password": "Changer le mot de passe",
"Current Password": "Mot de passe actuel",
"New Password": "Nouveau mot de passe",
"Repeat New Password": "Répéter votre nouveau mot de passe",
passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
"Update Password": "Mettre à jour le mot de passe",
"Disable Auth": "Désactiver l'authentification",
"Enable Auth": "Activer l'authentification",
Logout: "Se déconnecter",
notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.",
Leave: "Quitter",
"I understand, please disable": "J'ai compris, désactivez-le",
Confirm: "Confirmer",
Yes: "Oui",
No: "Non",
Username: "Nom d'utilisateur",
Password: "Mot de passe",
"Remember me": "Se souvenir de moi",
Login: "Se connecter",
"No Monitors, please": "Pas de sondes, veuillez ",
"add one": "en ajouter une.",
"Notification Type": "Type de notification",
Email: "Email",
Test: "Tester",
keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.",
"Certificate Info": "Informations sur le certificat SSL",
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
"Resolver Server": "Serveur DNS utilisé",
"Resource Record Type": "Type d'enregistrement DNS recherché",
resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
rrtypeDescription: "Veuillez séléctionner un type d'enregistrement DNS",
pauseMonitorMsg: "Etes vous sur de vouloir mettre en pause cette sonde ?",
"Last Result": "Dernier résultat",
"Create your admin account": "Créez votre compte administrateur",
"Repeat Password": "Répéter le mot de passe",
respTime: "Temps de réponse (ms)",
notAvailableShort: "N/A"
}

112
src/languages/ja.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "日本語",
checkEverySecond: "{0}秒ごとにチェックします。",
"Avg.": "平均 ",
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
maxRedirectDescription: "フォローするリダイレクトの最大数。リダイレクトを無効にするには0を設定する。",
acceptedStatusCodesDescription: "成功した応答とみなされるステータスコードを選択する。",
passwordNotMatchMsg: "繰り返しのパスワードが一致しません。",
notificationDescription: "監視を機能させるには、監視に通知を割り当ててください。",
keywordDescription: "プレーンHTMLまたはJSON応答でキーワードを検索し、大文字と小文字を区別します",
pauseDashboardHome: "一時停止",
deleteMonitorMsg: "この監視を削除してよろしいですか?",
deleteNotificationMsg: "全ての監視のこの通知を削除してよろしいですか?",
resoverserverDescription: "Cloudflareがデフォルトのサーバーですが、いつでもリゾルバサーバーを変更できます。",
rrtypeDescription: "監視するRRタイプを選択します",
pauseMonitorMsg: "一時停止しますか?",
Settings: "設定",
Dashboard: "ダッシュボード",
"New Update": "New Update",
Language: "言語",
Appearance: "外観",
Theme: "テーマ",
General: "General",
Version: "バージョン",
"Check Update On GitHub": "GitHubでアップデートを確認する",
List: "一覧",
Add: "追加",
"Add New Monitor": "監視の追加",
"Quick Stats": "統計",
Up: "Up",
Down: "Down",
Pending: "中止",
Unknown: "不明",
Pause: "一時停止",
Name: "名前",
Status: "ステータス",
DateTime: "日時",
Message: "メッセージ",
"No important events": "重要なイベントなし",
Resume: "再開",
Edit: "編集",
Delete: "削除",
Current: "現在",
Uptime: "起動時間",
"Cert Exp.": "証明書有効期限",
days: "日間",
day: "日",
"-day": "-日",
hour: "時間",
"-hour": "-時間",
Response: "レスポンス",
Ping: "Ping",
"Monitor Type": "監視タイプ",
Keyword: "キーワード",
"Friendly Name": "Friendly Name",
URL: "URL",
Hostname: "ホスト名",
Port: "ポート",
"Heartbeat Interval": "監視間隔",
Retries: "Retries",
Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "最大リダイレクト数",
"Accepted Status Codes": "承認されたステータスコード",
Save: "保存",
Notifications: "通知",
"Not available, please setup.": "利用できません。設定してください。",
"Setup Notification": "通知設定",
Light: "Light",
Dark: "Dark",
Auto: "Auto",
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
Normal: "通常",
Bottom: "下部",
None: "なし",
Timezone: "タイムゾーン",
"Search Engine Visibility": "検索エンジンでの表示",
"Allow indexing": "インデックス作成を許可する",
"Discourage search engines from indexing site": "検索エンジンにインデックスさせないようにする",
"Change Password": "パスワード変更",
"Current Password": "現在のパスワード",
"New Password": "新しいパスワード",
"Repeat New Password": "確認のため新しいパスワードをもう一度",
"Update Password": "パスワードの更新",
"Disable Auth": "認証の無効化",
"Enable Auth": "認証の有効化",
Logout: "ログアウト",
Leave: "作業を中止する",
"I understand, please disable": "理解した上で無効化する",
Confirm: "確認",
Yes: "はい",
No: "いいえ",
Username: "ユーザー名",
Password: "パスワード",
"Remember me": "パスワードを忘れた場合",
Login: "ログイン",
"No Monitors, please": "監視がありません",
"add one": "add one",
"Notification Type": "通知タイプ",
Email: "Eメール",
Test: "テスト",
"Certificate Info": "証明書情報",
"Resolver Server": "問い合わせ先DNSサーバ",
"Resource Record Type": "DNSレコード設定",
"Last Result": "最終結果",
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/ko-KR.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "한국어",
checkEverySecond: "{0} 초마다 체크해요.",
"Avg.": "평균 ",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
maxRedirectDescription: "최대 리다이렉트 횟수에요. 0을 입력하면 리다이렉트를 꺼요.",
acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.",
passwordNotMatchMsg: "비밀번호 재입력이 일치하지 않아요.",
notificationDescription: "모니터링에 알림을 설정할 수 있어요.",
keywordDescription: "Html 이나 JSON에서 대소문자를 구분해 키워드를 검색해요.",
pauseDashboardHome: "일시 정지",
deleteMonitorMsg: "정말 이 모니터링을 삭제할까요?",
deleteNotificationMsg: "정말 이 알림을 모든 모니터링에서 삭제할까요?",
resoverserverDescription: "Cloudflare가 기본 서버에요, 원한다면 언제나 다른 resolver 서버로 변경할 수 있어요.",
rrtypeDescription: "모니터링할 RR-Type을 선택해요.",
pauseMonitorMsg: "정말 이 모니터링을 일시 정지 할까요?",
Settings: "설정",
Dashboard: "대시보드",
"New Update": "새로운 업데이트",
Language: "언어",
Appearance: "외형",
Theme: "테마",
General: "일반",
Version: "버전",
"Check Update On GitHub": "깃허브에서 업데이트 확인",
List: "목록",
Add: "추가",
"Add New Monitor": "새로운 모니터링 추가하기",
"Quick Stats": "간단한 정보",
Up: "온라인",
Down: "오프라인",
Pending: "대기 중",
Unknown: "알 수 없음",
Pause: "일시 정지",
Name: "이름",
Status: "상태",
DateTime: "날짜",
Message: "메시지",
"No important events": "중요 이벤트 없음",
Resume: "재개",
Edit: "수정",
Delete: "삭제",
Current: "현재",
Uptime: "업타임",
"Cert Exp.": "인증서 만료",
days: "일",
day: "일",
"-day": "-일",
hour: "시간",
"-hour": "-시간",
Response: "응답",
Ping: "핑",
"Monitor Type": "모니터링 종류",
Keyword: "키워드",
"Friendly Name": "이름",
URL: "URL",
Hostname: "호스트네임",
Port: "포트",
"Heartbeat Interval": "하트비트 주기",
Retries: "재시도",
Advanced: "고급",
"Upside Down Mode": "상태 반전 모드",
"Max. Redirects": "최대 리다이렉트",
"Accepted Status Codes": "응답 성공 상태 코드",
Save: "저장",
Notifications: "알림",
"Not available, please setup.": "존재하지 않아요, 새로운거 하나 만드는건 어때요?",
"Setup Notification": "알림 설정",
Light: "라이트",
Dark: "다크",
Auto: "자동",
"Theme - Heartbeat Bar": "테마 - 하트비트 바",
Normal: "기본값",
Bottom: "가운데",
None: "제거",
Timezone: "시간대",
"Search Engine Visibility": "검색 엔진 활성화",
"Allow indexing": "인덱싱 허용",
"Discourage search engines from indexing site": "검색 엔진 인덱싱 거부",
"Change Password": "비밀번호 변경",
"Current Password": "기존 비밀번호",
"New Password": "새로운 비밀번호",
"Repeat New Password": "새로운 비밀번호 재입력",
"Update Password": "비밀번호 변경",
"Disable Auth": "인증 끄기",
"Enable Auth": "인증 켜기",
Logout: "로그아웃",
Leave: "나가기",
"I understand, please disable": "기능에 대해 이해했으니 꺼주세요.",
Confirm: "확인",
Yes: "확인",
No: "취소",
Username: "이름",
Password: "비밀번호",
"Remember me": "비밀번호 기억하기",
Login: "로그인",
"No Monitors, please": "모니터링이 없어요,",
"add one": "하나 추가해봐요",
"Notification Type": "알림 종류",
Email: "이메일",
Test: "테스트",
"Certificate Info": "인증서 정보",
"Resolver Server": "Resolver 서버",
"Resource Record Type": "자원 레코드 유형",
"Last Result": "최근 결과",
"Create your admin account": "관리자 계정 만들기",
"Repeat Password": "비밀번호 재입력",
respTime: "응답 시간 (ms)",
notAvailableShort: "N/A"
}

112
src/languages/nl-NL.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Nederlands",
checkEverySecond: "Controleer elke {0} seconden.",
"Avg.": "Gem. ",
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
maxRedirectDescription: "Maximaal aantal te volgen omleidingen. Stel in op 0 om omleidingen uit te schakelen.",
acceptedStatusCodesDescription: "Selecteer statuscodes die als een succesvol antwoord worden beschouwd.",
passwordNotMatchMsg: "Het herhaalwachtwoord komt niet overeen.",
notificationDescription: "Wijs a.u.b. een melding toe aan de monitor(s) om het te laten werken.",
keywordDescription: "Zoek trefwoord in gewone html of JSON-response en het is hoofdlettergevoelig",
pauseDashboardHome: "Gepauzeerd",
deleteMonitorMsg: "Weet u zeker dat u deze monitor wilt verwijderen?",
deleteNotificationMsg: "Weet u zeker dat u deze melding voor alle monitoren wilt verwijderen?",
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
Settings: "Instellingen",
Dashboard: "Dashboard",
"New Update": "Nieuwe update",
Language: "Taal",
Appearance: "Weergave",
Theme: "Thema",
General: "Algemeen",
Version: "Versie",
"Check Update On GitHub": "Controleer voor updates op GitHub",
List: "Lijst",
Add: "Toevoegen",
"Add New Monitor": "Nieuwe monitor toevoegen",
"Quick Stats": "Snelle statistieken",
Up: "Online",
Down: "Offline",
Pending: "In afwachting",
Unknown: "Onbekend",
Pause: "Pauze",
Name: "Naam",
Status: "Status",
DateTime: "Datum Tijd",
Message: "Bericht",
"No important events": "Geen belangrijke gebeurtenissen",
Resume: "Hervat",
Edit: "Wijzigen",
Delete: "Verwijderen",
Current: "Huidig",
Uptime: "Uptime",
"Cert Exp.": "Cert. verl.",
days: "dagen",
day: "dag",
"-day": "-dag",
hour: "uur",
"-hour": "-uur",
Response: "Antwoord",
Ping: "Ping",
"Monitor Type": "Monitortype:",
Keyword: "Trefwoord",
"Friendly Name": "Vriendelijke naam",
URL: "URL",
Hostname: "Hostnaam",
Port: "Poort",
"Heartbeat Interval": "Hartslaginterval",
Retries: "Pogingen",
Advanced: "Geavanceerd",
"Upside Down Mode": "Ondersteboven modus",
"Max. Redirects": "Max. Omleidingen",
"Accepted Status Codes": "Geaccepteerde statuscodes",
Save: "Opslaan",
Notifications: "Meldingen",
"Not available, please setup.": "Niet beschikbaar, stel a.u.b. in.",
"Setup Notification": "Melding instellen",
Light: "Licht",
Dark: "Donker",
Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Hartslagbalk",
Normal: "Normaal",
Bottom: "Onderkant",
None: "Geen",
Timezone: "Tijdzone",
"Search Engine Visibility": "Zichtbaarheid voor zoekmachines",
"Allow indexing": "Indexering toestaan",
"Discourage search engines from indexing site": "Ontmoedig zoekmachines om de site te indexeren",
"Change Password": "Verander wachtwoord",
"Current Password": "Huidig wachtwoord",
"New Password": "Nieuw wachtwoord",
"Repeat New Password": "Herhaal nieuw wachtwoord",
"Update Password": "Vernieuw wachtwoord",
"Disable Auth": "Autorisatie uitschakelen",
"Enable Auth": "Autorisatie inschakelen",
Logout: "Uitloggen",
Leave: "Vertrekken",
"I understand, please disable": "Ik begrijp het, schakel a.u.b. uit",
Confirm: "Bevestigen",
Yes: "Ja",
No: "Nee",
Username: "Gebruikersnaam",
Password: "Wachtwoord",
"Remember me": "Wachtwoord onthouden",
Login: "Inloggen",
"No Monitors, please": "Geen monitoren, ",
"add one": "voeg een toe",
"Notification Type": "Melding type",
Email: "E-mail",
Test: "Testen",
"Certificate Info": "Certificaat informatie",
"Resolver Server": "Resolver Server",
"Resource Record Type": "Type bronrecord",
"Last Result": "Laatste resultaat",
"Create your admin account": "Maak uw beheerdersaccount aan",
"Repeat Password": "Herhaal wachtwoord",
respTime: "resp. tijd (ms)",
notAvailableShort: "N.v.t."
}

112
src/languages/ru-RU.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Русский",
checkEverySecond: "Проверять каждые {0} секунд.",
"Avg.": "Средн. ",
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
maxRedirectDescription: "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
acceptedStatusCodesDescription: "Выберите коды статусов, которые должны считаться за успешный ответ.",
passwordNotMatchMsg: "Повтор пароля не совпадает.",
notificationDescription: "Привяжите уведомления к мониторам.",
keywordDescription: "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру)",
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Вы действительно хотите удалить данный монитор?",
deleteNotificationMsg: "Вы действительно хотите удалить это уведомление для всех мониторов?",
resoverserverDescription: "Cloudflare является сервером по умолчанию. Вы всегда можете сменить данный сервер.",
rrtypeDescription: "Выберите тип ресурсной записи, который вы хотите отслеживать",
pauseMonitorMsg: "Вы действительно хотите поставить на паузу?",
Settings: "Настройки",
Dashboard: "Панель",
"New Update": "Обновление",
Language: "Язык",
Appearance: "Внешний вид",
Theme: "Тема",
General: "Общее",
Version: "Версия",
"Check Update On GitHub": "Проверить обновления на GitHub",
List: "Список",
Add: "Добавить",
"Add New Monitor": "Новый монитор",
"Quick Stats": "Статистика",
Up: "Доступно",
Down: "Недоступно",
Pending: "Ожидание",
Unknown: "Неизвестно",
Pause: "Пауза",
Name: "Имя",
Status: "Статус",
DateTime: "Дата и время",
Message: "Сообщение",
"No important events": "Важных событий нет",
Resume: "Возобновить",
Edit: "Изменить",
Delete: "Удалить",
Current: "Текущий",
Uptime: "Аптайм",
"Cert Exp.": "Сертификат просрочен",
days: "дней",
day: "день",
"-day": " дней",
hour: "час",
"-hour": " часа",
Response: "Ответ",
Ping: "Пинг",
"Monitor Type": "Тип монитора",
Keyword: "Слово",
"Friendly Name": "Имя",
URL: "URL",
Hostname: "Имя хоста",
Port: "Порт",
"Heartbeat Interval": "Частота опроса",
Retries: "Попыток",
Advanced: "Дополнительно",
"Upside Down Mode": "Режим реверса статуса",
"Max. Redirects": "Макс. перенаправлений",
"Accepted Status Codes": "Допустимые коды статуса",
Save: "Сохранить",
Notifications: "Уведомления",
"Not available, please setup.": "Доступных уведомлений нет, необходима настройка.",
"Setup Notification": "Настроить уведомления",
Light: "Светлая",
Dark: "Тёмная",
Auto: "Авто",
"Theme - Heartbeat Bar": "Тема - Полоса частоты опроса",
Normal: "Обычный",
Bottom: "Снизу",
None: "Отсутствует",
Timezone: "Часовой пояс",
"Search Engine Visibility": "Видимость поисковым движком",
"Allow indexing": "Разрешить индексирование",
"Discourage search engines from indexing site": "Не позволять индексировать сайт",
"Change Password": "Сменить пароль",
"Current Password": "Текущий пароль",
"New Password": "Новый пароль",
"Repeat New Password": "Повтор нового пароля",
"Update Password": "Обновить пароль",
"Disable Auth": "Отключить авторизацию",
"Enable Auth": "Включить авторизацию",
Logout: "Выйти",
Leave: "Отмена",
"I understand, please disable": "Я понимаю, всё равно отключить",
Confirm: "Подтвердить",
Yes: "Да",
No: "Нет",
Username: "Логин",
Password: "Пароль",
"Remember me": "Запомнить меня",
Login: "Вход в систему",
"No Monitors, please": "Мониторов нет, пожалуйста",
"add one": "создайте новый",
"Notification Type": "Тип уведомления",
Email: "Почта",
Test: "Проверка",
"Certificate Info": "Информация о сертификате",
"Resolver Server": "DNS сервер",
"Resource Record Type": "Тип ресурсной записи",
"Last Result": "Последний результат",
"Create your admin account": "Создайте аккаунт администратора",
"Repeat Password": "Повторите пароль",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/sr-latn.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Srpski",
checkEverySecond: "Proveri svakih {0} sekundi.",
"Avg.": "Prosečni ",
retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",
maxRedirectDescription: "Maksimani broj preusmerenja da se prate. Postavite na 0 da bi se isključila preusmerenja.",
acceptedStatusCodesDescription: "Odaberite statusne kodove koji se smatraju uspešnim odgovorom.",
passwordNotMatchMsg: "Ponovljena lozinka se ne poklapa.",
notificationDescription: "Molim Vas postavite obaveštenje za masmatrače da bise aktivirali.",
keywordDescription: "Pretraži ključnu reč u čistom html ili JSON odgovoru sa osetljivim velikim i malim slovima",
pauseDashboardHome: "Pauziraj",
deleteMonitorMsg: "Da li ste sigurni da želite da obrišete ovog posmatrača?",
deleteNotificationMsg: "Da li ste sigurni d aželite da uklonite ovo obaveštenje za sve posmatrače?",
resoverserverDescription: "Cloudflare je podrazumevani server. Možete promeniti server za raszrešavanje u bilo kom trenutku.",
rrtypeDescription: "Odaberite RR-Type koji želite da posmatrate",
pauseMonitorMsg: "Da li ste sigurni da želite da pauzirate?",
Settings: "Podešavanja",
Dashboard: "Komandna tabla",
"New Update": "Nova verzija",
Language: "Jezik",
Appearance: "Izgled",
Theme: "Tema",
General: "Opšte",
Version: "Verzija",
"Check Update On GitHub": "Proverite novu verziju na GitHub-u",
List: "Lista",
Add: "Dodaj",
"Add New Monitor": "Dodaj novog posmatrača",
"Quick Stats": "Brze statistike",
Up: "Aktivno",
Down: "Neaktivno",
Pending: "Nerešeno",
Unknown: "Nepoznato",
Pause: "Pauziraj",
Name: "Ime",
Status: "Status",
DateTime: "Datum i vreme",
Message: "Poruka",
"No important events": "Nema bitnih događaja",
Resume: "Nastavi",
Edit: "Izmeni",
Delete: "Ukloni",
Current: "Trenutno",
Uptime: "Vreme rada",
"Cert Exp.": "Istek sert.",
days: "dana",
day: "dan",
"-day": "-dana",
hour: "sat",
"-hour": "-sata",
Response: "Odgovor",
Ping: "Ping",
"Monitor Type": "Tip posmatrača",
Keyword: "Ključna reč",
"Friendly Name": "Prijateljsko ime",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Interval otkucaja srca",
Retries: "Pokušaji",
Advanced: "Napredno",
"Upside Down Mode": "Naopak mod",
"Max. Redirects": "Maks. preusmerenja",
"Accepted Status Codes": "Prihvaćeni statusni kodovi",
Save: "Sačuvaj",
Notifications: "Obaveštenja",
"Not available, please setup.": "Nije dostupno, molim Vas podesite.",
"Setup Notification": "Postavi obaveštenje",
Light: "Svetlo",
Dark: "Tamno",
Auto: "Automatsko",
"Theme - Heartbeat Bar": "Tema - Traka otkucaja srca",
Normal: "Normalno",
Bottom: "Dole",
None: "Isključeno",
Timezone: "Vremenska zona",
"Search Engine Visibility": "Vidljivost pretraživačima",
"Allow indexing": "Dozvoli indeksiranje",
"Discourage search engines from indexing site": "Odvraćajte pretraživače od indeksiranja sajta",
"Change Password": "Promeni lozinku",
"Current Password": "Trenutna lozinka",
"New Password": "Nova lozinka",
"Repeat New Password": "Ponovi novu lozinku",
"Update Password": "Izmeni lozinku",
"Disable Auth": "Isključi autentifikaciju",
"Enable Auth": "Uključi autentifikaciju",
Logout: "Odloguj se",
Leave: "Izađi",
"I understand, please disable": "Razumem, molim te isključi",
Confirm: "Potvrdi",
Yes: "Da",
No: "Ne",
Username: "Korisničko ime",
Password: "Lozinka",
"Remember me": "Zapamti me",
Login: "Uloguj se",
"No Monitors, please": "Bez posmatrača molim",
"add one": "dodaj jednog",
"Notification Type": "Tip obaveštenja",
Email: "E-pošta",
Test: "Test",
"Certificate Info": "Informacije sertifikata",
"Resolver Server": "Razrešivački server",
"Resource Record Type": "Tip zapisa resursa",
"Last Result": "Poslednji rezultat",
"Create your admin account": "Naprivi administratorski nalog",
"Repeat Password": "Ponovite lozinku",
respTime: "Vreme odg. (ms)",
notAvailableShort: "N/A"
}

112
src/languages/sr.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Српски",
checkEverySecond: "Провери сваких {0} секунди.",
"Avg.": "Просечни ",
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
maxRedirectDescription: "Максимани број преусмерења да се прате. Поставите на 0 да би се искључила преусмерења.",
acceptedStatusCodesDescription: "Одаберите статусне кодове који се сматрају успешним одговором.",
passwordNotMatchMsg: "Поновљена лозинка се не поклапа.",
notificationDescription: "Молим Вас поставите обавештење за масматраче да бисе активирали.",
keywordDescription: "Претражи кључну реч у чистом html или JSON одговору са осетљивим великим и малим словима",
pauseDashboardHome: "Паузирај",
deleteMonitorMsg: "Да ли сте сигурни да желите да обришете овог посматрача?",
deleteNotificationMsg: "Да ли сте сигурни д ажелите да уклоните ово обавештење за све посматраче?",
resoverserverDescription: "Cloudflare је подразумевани сервер. Можете променити сервер за расзрешавање у било ком тренутку.",
rrtypeDescription: "Одаберите RR-Type који желите да посматрате",
pauseMonitorMsg: "Да ли сте сигурни да желите да паузирате?",
Settings: "Подешавања",
Dashboard: "Командна табла",
"New Update": "Нова верзија",
Language: "Језик",
Appearance: "Изглед",
Theme: "Тема",
General: "Опште",
Version: "Верзија",
"Check Update On GitHub": "Проверите нову верзију на GitHub-у",
List: "Листа",
Add: "Додај",
"Add New Monitor": "Додај новог посматрача",
"Quick Stats": "Брзе статистике",
Up: "Активно",
Down: "Неактивно",
Pending: "Нерешено",
Unknown: "Непознато",
Pause: "Паузирај",
Name: "Име",
Status: "Статус",
DateTime: "Датум и време",
Message: "Порука",
"No important events": "Нема битних догађаја",
Resume: "Настави",
Edit: "Измени",
Delete: "Уклони",
Current: "Тренутно",
Uptime: "Време рада",
"Cert Exp.": "Истек серт.",
days: "дана",
day: "дан",
"-day": "-дана",
hour: "сат",
"-hour": "-сата",
Response: "Одговор",
Ping: "Пинг",
"Monitor Type": "Тип посматрача",
Keyword: "Кључна реч",
"Friendly Name": "Пријатељско име",
URL: "URL",
Hostname: "Hostname",
Port: "Порт",
"Heartbeat Interval": "Интервал откуцаја срца",
Retries: "Покушаји",
Advanced: "Напредно",
"Upside Down Mode": "Наопак мод",
"Max. Redirects": "Макс. преусмерења",
"Accepted Status Codes": "Прихваћени статусни кодови",
Save: "Сачувај",
Notifications: "Обавештења",
"Not available, please setup.": "Није доступно, молим Вас подесите.",
"Setup Notification": "Постави обавештење",
Light: "Светло",
Dark: "Тамно",
Auto: "Аутоматско",
"Theme - Heartbeat Bar": "Тема - Трака откуцаја срца",
Normal: "Нормално",
Bottom: "Доле",
None: "Искључено",
Timezone: "Временска зона",
"Search Engine Visibility": "Видљивост претраживачима",
"Allow indexing": "Дозволи индексирање",
"Discourage search engines from indexing site": "Одвраћајте претраживаче од индексирања сајта",
"Change Password": "Промени лозинку",
"Current Password": "Тренутна лозинка",
"New Password": "Нова лозинка",
"Repeat New Password": "Понови нову лозинку",
"Update Password": "Измени лозинку",
"Disable Auth": "Искључи аутентификацију",
"Enable Auth": "Укључи аутентификацију",
Logout: "Одлогуј се",
Leave: "Изађи",
"I understand, please disable": "Разумем, молим те искључи",
Confirm: "Потврди",
Yes: "Да",
No: "Не",
Username: "Корисничко име",
Password: "Лозинка",
"Remember me": "Запамти ме",
Login: "Улогуј се",
"No Monitors, please": "Без посматрача молим",
"add one": "додај једног",
"Notification Type": "Тип обавештења",
Email: "Е-пошта",
Test: "Тест",
"Certificate Info": "Информације сертификата",
"Resolver Server": "Разрешивачки сервер",
"Resource Record Type": "Тип записа ресурса",
"Last Result": "Последњи резултат",
"Create your admin account": "Наприви администраторски налог",
"Repeat Password": "Поновите лозинку",
respTime: "Време одг. (мс)",
notAvailableShort: "N/A"
}

112
src/languages/sv-SE.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "Swedish",
checkEverySecond: "Uppdatera var {0} sekund.",
"Avg.": "Genomsnitt ",
retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",
maxRedirectDescription: "Max antal omdirigeringar att följa. Välj 0 för att avaktivera omdirigeringar.",
acceptedStatusCodesDescription: "Välj statuskoder som räknas som lyckade.",
passwordNotMatchMsg: "Det bekräftade lösenordet stämmer ej överens.",
notificationDescription: "Vänligen lägg till en notistjänst till övervakaren.",
keywordDescription: "Sök efter nyckelord i ren HTML eller JSON-svar. Sökningen är skiftkänslig.",
pauseDashboardHome: "Pausa",
deleteMonitorMsg: "Är du säker på att du vill ta bort den här övervakningen?",
deleteNotificationMsg: "Är du säker på att du vill ta bort den här notisen för alla övervakare?",
resoverserverDescription: "Cloudflare är den förvalda servern. Du kan byta resolver när som helst.",
rrtypeDescription: "Välj den RR-typ du vill övervaka",
pauseMonitorMsg: "Är du säker på att du vill pausa?",
Settings: "Inställningar",
Dashboard: "Infopanel",
"New Update": "Ny uppdatering",
Language: "Språk",
Appearance: "Utseende",
Theme: "Tema",
General: "Allmänt",
Version: "Version",
"Check Update On GitHub": "Sök efter uppdatering på GitHub",
List: "Lista",
Add: "Lägg till",
"Add New Monitor": "Lägg Till Ny Övervakare",
"Quick Stats": "Snabbstatistik",
Up: "Uppe",
Down: "Nere",
Pending: "Pågående",
Unknown: "Okänt",
Pause: "Paus",
Name: "Namn",
Status: "Status",
DateTime: "DatumTid",
Message: "Meddelande",
"No important events": "Inga viktiga händelser",
Resume: "Återuppta",
Edit: "Redigera",
Delete: "Ta bort",
Current: "Nuvarande",
Uptime: "Drifttid",
"Cert Exp.": "Certifikatsutgång",
days: "dagar",
day: "dag",
"-day": "-dag",
hour: "timme",
"-hour": "-timme",
Response: "Svar",
Ping: "Ping",
"Monitor Type": "Övervakningstyp",
Keyword: "Nyckelord",
"Friendly Name": "Vänligt Namn",
URL: "URL",
Hostname: "Värdnamn",
Port: "Port",
"Heartbeat Interval": "Hjärtslagsintervall",
Retries: "Försök",
Advanced: "Avancerat",
"Upside Down Mode": "Upp och ner-läge",
"Max. Redirects": "Max antal omdirigeringar",
"Accepted Status Codes": "Tillåtna statuskoder",
Save: "Spara",
Notifications: "Notiser",
"Not available, please setup.": "Ej tillgänglig, vänligen konfigurera.",
"Setup Notification": "Konfigurera Notis",
Light: "Ljust",
Dark: "Mörkt",
Auto: "Automatisk",
"Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
Normal: "Normal",
Bottom: "Botten",
None: "Ingen",
Timezone: "Tidszon",
"Search Engine Visibility": "Synlighet på Sökmotorer",
"Allow indexing": "Tillåt indexering",
"Discourage search engines from indexing site": "Hindra sökmotorer från att indexera sidan",
"Change Password": "Byt Lösenord",
"Current Password": "Nuvarande Lösenord",
"New Password": "Nytt Lösenord",
"Repeat New Password": "Upprepa Nytt Lösenord",
"Update Password": "Uppdatera Lösenord",
"Disable Auth": "Avaktivera Autentisering",
"Enable Auth": "Aktivera Autentisering",
Logout: "Logga ut",
Leave: "Lämna",
"I understand, please disable": "Jag förstår, vänligen avaktivera",
Confirm: "Bekräfta",
Yes: "Ja",
No: "Nej",
Username: "Användarnamn",
Password: "Lösenord",
"Remember me": "Kom ihåg mig",
Login: "Logga in",
"No Monitors, please": "Inga Övervakare, tack",
"add one": "lägg till en",
"Notification Type": "Notistyp",
Email: "Email",
Test: "Test",
"Certificate Info": "Certifikatsinfo",
"Resolver Server": "Resolverserver",
"Resource Record Type": "RR-typ",
"Last Result": "Senaste resultat",
"Create your admin account": "Skapa ditt administratörskonto",
"Repeat Password": "Upprepa Lösenord",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/zh-CN.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "简体中文",
checkEverySecond: "检测频率 {0} 秒",
"Avg.": "平均",
retriesDescription: "最大重试失败次数",
ignoreTLSError: "忽略HTTPS站点的证书错误",
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
maxRedirectDescription: "最大重定向次数,设置为 0 禁止重定向",
acceptedStatusCodesDescription: "选择被视为成功响应的状态码",
passwordNotMatchMsg: "两次密码输入不一致",
notificationDescription: "请为监控项配置消息通知",
keywordDescription: "检测响应内容中的关键字,区分大小写",
pauseDashboardHome: "暂停",
deleteMonitorMsg: "确定要删除此监控吗?",
deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。",
resoverserverDescription: "默认服务器 Cloudflare可以修改为任意你想要使用的DNS服务器",
rrtypeDescription: "选择要监控的资源记录类型",
pauseMonitorMsg: "确定要暂停吗?",
Settings: "设置",
Dashboard: "仪表盘",
"New Update": "有新版本更新",
Language: "语言",
Appearance: "外观设置",
Theme: "主题",
General: "基本设置",
Version: "Version",
"Check Update On GitHub": "检查更新",
List: "列表",
Add: "添加",
"Add New Monitor": "创建监控项",
"Quick Stats": "状态速览",
Up: "正常",
Down: "故障",
Pending: "检测失败",
Unknown: "未知",
Pause: "暂停",
Name: "名称",
Status: "状态",
DateTime: "时间",
Message: "事件",
"No important events": "暂无重要事件",
Resume: "恢复",
Edit: "修改",
Delete: "删除",
Current: "当前",
Uptime: "可用率",
"Cert Exp.": "证书过期",
days: "天",
day: "天",
"-day": " 天",
hour: "小时",
"-hour": " 小时",
Response: "响应时长",
Ping: "Ping",
"Monitor Type": "监控类型",
Keyword: "关键字",
"Friendly Name": "自定义名称",
URL: "网址URL",
Hostname: "主机名",
Port: "端口号",
"Heartbeat Interval": "心跳间隔",
Retries: "重试次数",
Advanced: "高级选项",
"Upside Down Mode": "反向监控",
"Max. Redirects": "重定向次数",
"Accepted Status Codes": "有效状态码",
Save: "保存",
Notifications: "消息通知",
"Not available, please setup.": "无可用通道,请先设置",
"Setup Notification": "设置通知",
Light: "明亮",
Dark: "黑暗",
Auto: "自动",
"Theme - Heartbeat Bar": "状态显示",
Normal: "正常显示",
Bottom: "靠下显示",
None: "不显示",
Timezone: "时区",
"Search Engine Visibility": "搜索引擎设置",
"Allow indexing": "允许索引",
"Discourage search engines from indexing site": "阻止搜索引擎索引网站",
"Change Password": "修改密码",
"Current Password": "当前密码",
"New Password": "新的密码",
"Repeat New Password": "重复新的密码",
"Update Password": "更新密码",
"Disable Auth": "禁用身份验证",
"Enable Auth": "启用身份验证",
Logout: "退出",
Leave: "离开",
"I understand, please disable": "我已了解,继续禁用",
Confirm: "确认",
Yes: "确定",
No: "取消",
Username: "用户名",
Password: "密码",
"Remember me": "记住登录",
Login: "登录",
"No Monitors, please": "还没有监控项,",
"add one": "点击新增",
"Notification Type": "消息类型",
Email: "邮件",
Test: "测试一下",
"Certificate Info": "证书信息",
"Resolver Server": "解析服务器",
"Resource Record Type": "资源记录类型",
"Last Result": "Last Result",
"Create your admin account": "创建管理员账号",
"Repeat Password": "重复密码",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}

112
src/languages/zh-HK.js Normal file
View File

@@ -0,0 +1,112 @@
export default {
languageName: "繁體中文 (香港)",
Settings: "設定",
Dashboard: "錶板",
"New Update": "有更新",
Language: "語言",
Appearance: "外觀",
Theme: "主題",
General: "一般",
Version: "版本",
"Check Update On GitHub": "到 Github 查看更新",
List: "列表",
Add: "新增",
"Add New Monitor": "新增監測器",
"Quick Stats": "綜合數據",
Up: "上線",
Down: "離線",
Pending: "待定",
Unknown: "不明",
Pause: "暫停",
pauseDashboardHome: "暫停",
Name: "名稱",
Status: "狀態",
DateTime: "日期時間",
Message: "內容",
"No important events": "沒有重要事件",
Resume: "恢復",
Edit: "編輯",
Delete: "刪除",
Current: "目前",
Uptime: "上線率",
"Cert Exp.": "証書期限",
days: "日",
day: "日",
"-day": "日",
hour: "小時",
"-hour": "小時",
checkEverySecond: "每 {0} 秒檢查一次",
"Avg.": "平均",
Response: "反應時間",
Ping: "反應時間",
"Monitor Type": "監測器類型",
Keyword: "關鍵字",
"Friendly Name": "名稱",
URL: "網址 URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "檢查間距",
Retries: "重試數次確定為離線",
retriesDescription: "重試多少次後才判定為離線及傳送通知。如數值為 0 會即判定為離線及傳送通知。",
Advanced: "進階",
ignoreTLSError: "忽略 TLS/SSL 錯誤",
"Upside Down Mode": "反轉模式",
upsideDownModeDescription: "反轉狀態,如網址是可正常瀏覽,會被判定為 '離線/DOWN'",
"Max. Redirects": "跟隨重新導向 (Redirect) 的次數",
maxRedirectDescription: "設為 0 即不跟蹤",
"Accepted Status Codes": "接受為上線的 HTTP 狀態碼",
acceptedStatusCodesDescription: "可多選",
Save: "儲存",
Notifications: "通知",
"Not available, please setup.": "無法使用,需要設定",
"Setup Notification": "設定通知",
Light: "明亮",
Dark: "暗黑",
Auto: "自動",
"Theme - Heartbeat Bar": "監測器列表 狀態條外觀",
Normal: "一般",
Bottom: "下方",
None: "沒有",
Timezone: "時區",
"Search Engine Visibility": "是否允許搜尋器索引",
"Allow indexing": "允許索引",
"Discourage search engines from indexing site": "不建議搜尋器索引",
"Change Password": "變更密碼",
"Current Password": "目前密碼",
"New Password": "新密碼",
"Repeat New Password": "確認新密碼",
passwordNotMatchMsg: "密碼不一致",
"Update Password": "更新密碼",
"Disable Auth": "取消登入認証",
"Enable Auth": "開啟登入認証",
Logout: "登出",
notificationDescription: "新增後,你需要在監測器裡啟用。",
Leave: "離開",
"I understand, please disable": "我明白,請取消登入認証",
Confirm: "確認",
Yes: "是",
No: "否",
Username: "帳號",
Password: "密碼",
"Remember me": "記住我",
Login: "登入",
"No Monitors, please": "沒有監測器,請",
"add one": "新增",
"Notification Type": "通知類型",
Email: "電郵",
Test: "測試",
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
"Certificate Info": "憑證詳細資料",
deleteMonitorMsg: "是否確定刪除這個監測器",
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
"Resolver Server": "DNS 伺服器",
"Resource Record Type": "DNS 記錄類型",
resoverserverDescription: "預設值為 Cloudflare DNS 伺服器,你可以轉用其他 DNS 伺服器。",
rrtypeDescription: "請選擇 DNS 記錄類型",
pauseMonitorMsg: "是否確定暫停?",
"Last Result": "最後結果",
"Create your admin account": "製作你的管理員帳號",
"Repeat Password": "重複密碼",
respTime: "反應時間 (ms)",
notAvailableShort: "N/A"
}

View File

@@ -1,109 +1,131 @@
<template>
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid">
{{ $root.connectionErrorMsg }}
<div :class="classes">
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid">
{{ $root.connectionErrorMsg }}
</div>
</div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
</a>
<ul class="nav nav-pills">
<li class="nav-item">
<router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
</router-link>
</li>
<li class="nav-item">
<router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> {{ $t("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>
<router-view v-if="$root.loggedIn" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
<!-- 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>
{{ $t("Dashboard") }}
</router-link>
<router-link to="/list" class="nav-link">
<div><font-awesome-icon icon="list" /></div>
{{ $t("List") }}
</router-link>
<router-link to="/add" class="nav-link">
<div><font-awesome-icon icon="plus" /></div>
{{ $t("Add") }}
</router-link>
<router-link to="/settings" class="nav-link">
<div><font-awesome-icon icon="cog" /></div>
{{ $t("Settings") }}
</router-link>
</nav>
</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>
<script>
import Login from "../components/Login.vue";
import compareVersions from "compare-versions";
export default {
components: {
Login,
},
data() {
return {}
},
computed: {},
computed: {
// Theme or Mobile
classes() {
const classes = {};
classes[this.$root.theme] = true;
classes["mobile"] = this.$root.isMobile;
return classes;
},
hasNewVersion() {
if (this.$root.info.latestVersion && this.$root.info.version) {
return compareVersions(this.$root.info.latestVersion, this.$root.info.version) >= 1;
} else {
return false;
}
},
},
watch: {
$route (to, from) {
$route(to, from) {
this.init();
},
},
mounted() {
this.init();
},
methods: {
init() {
if (this.$route.name === "root") {
this.$router.push("/dashboard")
}
},
},
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars.scss";
.bottom-nav {
@@ -141,6 +163,10 @@ export default {
}
}
main {
min-height: calc(100vh - 160px);
}
.title {
font-weight: bold;
}
@@ -155,13 +181,19 @@ export default {
color: white;
}
footer {
color: #AAA;
font-size: 13px;
margin-top: 10px;
margin-bottom: 30px;
margin-left: 10px;
text-align: center;
.dark {
header {
background-color: #161b22;
border-bottom-color: #161b22 !important;
span {
color: #f0f6fc;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style>

View File

@@ -1,5 +1,6 @@
import "bootstrap";
import { createApp, h } from "vue";
import { createI18n } from "vue-i18n"
import { createRouter, createWebHistory } from "vue-router";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
@@ -9,12 +10,33 @@ import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue";
import socket from "./mixins/socket";
import theme from "./mixins/theme";
import mobile from "./mixins/mobile";
import datetime from "./mixins/datetime";
import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue";
import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue";
import List from "./pages/List.vue";
import { appName } from "./util.ts";
import en from "./languages/en";
import zhHK from "./languages/zh-HK";
import deDE from "./languages/de-DE";
import nlNL from "./languages/nl-NL";
import esEs from "./languages/es-ES";
import fr from "./languages/fr";
import ja from "./languages/ja";
import daDK from "./languages/da-DK";
import sr from "./languages/sr";
import srLatn from "./languages/sr-latn";
import svSE from "./languages/sv-SE";
import koKR from "./languages/ko-KR";
import ruRU from "./languages/ru-RU";
import zhCN from "./languages/zh-CN";
const routes = [
{
@@ -49,6 +71,10 @@ const routes = [
path: "/add",
component: EditMonitor,
},
{
path: "/list",
component: List,
},
],
},
{
@@ -73,14 +99,48 @@ const router = createRouter({
routes,
})
const languageList = {
en,
"zh-HK": zhHK,
"de-DE": deDE,
"nl-NL": nlNL,
"es-ES": esEs,
"fr": fr,
"ja": ja,
"da-DK": daDK,
"sr": sr,
"sr-latn": srLatn,
"sv-SE": svSE,
"ko-KR": koKR,
"ru-RU": ruRU,
"zh-CN": zhCN,
};
const i18n = createI18n({
locale: localStorage.locale || "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: true,
messages: languageList
});
const app = createApp({
mixins: [
socket,
theme,
mobile,
datetime
],
data() {
return {
appName: appName
}
},
render: () => h(App),
})
app.use(router)
app.use(router);
app.use(i18n);
const options = {
position: "bottom-right",

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 { useToast } from "vue-toastification";
const toast = useToast()
@@ -17,7 +16,6 @@ export default {
connectCount: 0,
},
remember: (localStorage.remember !== "0"),
userTimezone: localStorage.timezone || "auto",
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false,
monitorList: { },
@@ -27,9 +25,7 @@ export default {
uptimeList: { },
certInfoList: {},
notificationList: [],
windowWidth: window.innerWidth,
showListMobile: false,
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
}
},
@@ -189,14 +185,6 @@ export default {
methods: {
cancelActiveList() {
this.$root.showListMobile = false;
},
onResize() {
this.windowWidth = window.innerWidth;
},
storage() {
return (this.remember) ? localStorage : sessionStorage;
},
@@ -270,19 +258,6 @@ export default {
computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
timezone() {
if (this.userTimezone === "auto") {
return dayjs.tz.guess()
}
return this.userTimezone
},
lastHeartbeatList() {
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,33 +1,16 @@
<template>
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile">
<router-link to="/add" class="btn btn-primary"><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 v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
<div>
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
</div>
<MonitorList :scrollbar="true" />
</div>
<div class="col-12 col-md-7 col-xl-8">
<router-view />
<div class="col-12 col-md-7 col-xl-8 mb-3">
<!-- Add :key to disable vue router re-use the same component -->
<router-view :key="$route.fullPath" />
</div>
</div>
</div>
@@ -35,102 +18,20 @@
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
import MonitorList from "../components/MonitorList.vue";
export default {
components: {
Uptime,
HeartbeatBar,
MonitorList,
},
data() {
return {}
},
computed: {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
})
return result;
},
showList() {
return ! this.$root.isMobile || this.$root.showListMobile;
},
},
methods: {
monitorURL(id) {
return "/dashboard/" + id;
},
},
}
</script>
<style scoped lang="scss">
@import "../assets/vars.scss";
<style lang="scss" scoped>
.container-fluid {
width: 98%
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>

View File

@@ -1,78 +1,67 @@
<template>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">
Quick Stats
</h1>
<transition name="slide-fade" appear>
<div v-if="$route.name === 'DashboardHome'">
<h1 class="mb-3">
{{ $t("Quick Stats") }}
</h1>
<div class="shadow-box big-padding text-center">
<div class="row">
<div class="col">
<h3>Up</h3>
<span class="num">{{ stats.up }}</span>
</div>
<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 class="shadow-box big-padding text-center mb-4">
<div class="row">
<div class="col">
<h3>{{ $t("Up") }}</h3>
<span class="num">{{ stats.up }}</span>
</div>
<div class="col">
<h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ stats.down }}</span>
</div>
<div class="col">
<h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
</div>
<div class="col">
<h3>{{ $t("pauseDashboardHome") }}</h3>
<span class="num text-secondary">{{ stats.pause }}</span>
</div>
</div>
</div>
<div v-if="false" class="row">
<div class="col-3">
<h3>Uptime</h3>
<p>(24-hour)</p>
<span class="num" />
</div>
<div class="col-3">
<h3>Uptime</h3>
<p>(30-day)</p>
<span class="num" />
<div class="shadow-box table-shadow-box" style="overflow-x: scroll;">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>{{ $t("Name") }}</th>
<th>{{ $t("Status") }}</th>
<th>{{ $t("DateTime") }}</th>
<th>{{ $t("Message") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
<td>{{ beat.name }}</td>
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="4">
{{ $t("No important events") }}
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div>
</div>
<div class="shadow-box" style="margin-top: 25px;">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="(beat, 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>
</transition>
<router-view ref="child" />
</template>
@@ -169,7 +158,7 @@ export default {
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars";
.num {

View File

@@ -1,160 +1,183 @@
<template>
<h1> {{ monitor.name }}</h1>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
</span>
</p>
<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>
<transition name="slide-fade" appear>
<div v-if="monitor">
<h1> {{ monitor.name }}</h1>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
</span>
</div>
</div>
</div>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
</span>
</p>
<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">
<div class="functions">
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button>
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
</router-link>
<button class="btn btn-danger" @click="deleteDialog">
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
</button>
</div>
<div class="shadow-box">
<div class="row">
<div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
</div>
<div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
</div>
</div>
</div>
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div class="col">
<h4>{{ pingTitle }}</h4>
<p>({{ $t("Current") }})</p>
<span class="num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<CountUp :value="ping" />
</a>
</span>
</div>
<div class="col">
<h4>{{ $t("Avg.") }}{{ pingTitle }}</h4>
<p>(24{{ $t("-hour") }})</p>
<span class="num"><CountUp :value="avgPing" /></span>
</div>
<div class="col">
<h4>{{ $t("Uptime") }}</h4>
<p>(24{{ $t("-hour") }})</p>
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
</div>
<div class="col">
<h4>{{ $t("Uptime") }}</h4>
<p>(30{{ $t("-day") }})</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div>
<div v-if="certInfo" class="col">
<h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
<span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} {{ $t("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>{{ $t("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-shadow-box">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>{{ $t("Status") }}</th>
<th>{{ $t("DateTime") }}</th>
<th>{{ $t("Message") }}</th>
</tr>
</thead>
<tbody>
<tr class="my-3">
<td class="px-3">
Valid:
</td>
<td>{{ certInfo.valid }}</td>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}" style="padding: 10px;">
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
</tr>
<tr class="my-3">
<td class="px-3">
Valid To:
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">
{{ $t("No important events") }}
</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 class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMonitor">
{{ $t("pauseMonitorMsg") }}
</Confirm>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMonitor">
{{ $t("deleteMonitorMsg") }}
</Confirm>
</div>
</div>
<div class="shadow-box">
<table class="table table-borderless table-hover">
<thead>
<tr>
<th>Status</th>
<th>DateTime</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index">
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<td colspan="3">
No important events
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:per-page="perPage"
/>
</div>
</div>
<Confirm ref="confirmPause" @yes="pauseMonitor">
Are you sure want to pause?
</Confirm>
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
Are you sure want to delete this monitor?
</Confirm>
</transition>
</template>
<script>
import { defineAsyncComponent } from "vue";
import { useToast } from "vue-toastification"
const toast = useToast()
import Confirm from "../components/Confirm.vue";
@@ -164,6 +187,7 @@ import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
export default {
components: {
@@ -174,6 +198,7 @@ export default {
Confirm,
Status,
Pagination,
PingChart,
},
data() {
return {
@@ -181,16 +206,16 @@ export default {
perPage: 25,
heartBeatList: [],
toggleCertInfoBox: false,
showPingChartBox: true,
}
},
computed: {
pingTitle() {
if (this.monitor.type === "http") {
return "Response"
return this.$t("Response");
}
return "Ping"
return this.$t("Ping");
},
monitor() {
@@ -213,7 +238,7 @@ export default {
return this.lastHeartBeat.ping;
}
return "N/A"
return this.$t("notAvailableShort")
},
avgPing() {
@@ -221,7 +246,7 @@ export default {
return this.$root.avgPingList[this.monitor.id];
}
return "N/A"
return this.$t("notAvailableShort")
},
importantHeartBeatList() {
@@ -306,6 +331,41 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
@media (max-width: 767px) {
.badge {
margin-top: 14px;
}
}
@media (max-width: 550px) {
.functions {
text-align: center;
}
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
}
.ping-chart-wrapper {
padding: 10px !important;
}
}
@media (max-width: 400px) {
.btn {
display: inline-flex;
flex-direction: column;
align-items: center;
padding-top: 10px;
}
a.btn {
padding-left: 25px;
padding-right: 25px;
}
}
.url {
color: $primary;
margin-bottom: 20px;
@@ -328,7 +388,7 @@ export default {
}
.word {
color: #AAA;
color: #aaa;
font-size: 14px;
}
@@ -342,7 +402,7 @@ table {
.stats p {
font-size: 13px;
color: #AAA;
color: #aaa;
}
.stats {
@@ -352,4 +412,15 @@ table {
margin: 20px 0;
}
}
.keyword {
color: black;
}
.dark {
.keyword {
color: $dark-font-color;
}
}
</style>

View File

@@ -1,146 +1,239 @@
<template>
<h1 class="mb-3">
{{ pageName }}
</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2>General</h2>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">{{ pageName }}</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">{{ $t("General") }}</h2>
<div class="mb-3">
<label for="type" class="form-label">Monitor Type</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - Keyword
</option>
</select>
</div>
<div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http">
HTTP(s)
</option>
<option value="port">
TCP Port
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
</option>
<option value="dns">
DNS
</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<div class="my-3">
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3">
<label for="url" class="form-label">URL</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
<div v-if="monitor.type === 'keyword' " class="mb-3">
<label for="keyword" class="form-label">Keyword</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">
Search keyword in plain html or JSON response and it is case-sensitive
<div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">
{{ $t("keywordDescription") }}
</div>
</div>
<!-- TCP Port / Ping / DNS only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div>
<!-- For TCP Port Type -->
<div v-if="monitor.type === 'port' " class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<!-- For DNS Type -->
<template v-if="monitor.type === 'dns'">
<div class="my-3">
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server") }}</label>
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
<div class="form-text">
{{ $t("resoverserverDescription") }}
</div>
</div>
<div class="my-3">
<label for="dns_resolve_type" class="form-label">{{ $t("Resource Record Type") }}</label>
<!-- :allow-empty="false" is not working, set a default value instead https://github.com/shentao/vue-multiselect/issues/336 -->
<VueMultiselect
id="dns_resolve_type"
v-model="monitor.dns_resolve_type"
:options="dnsresolvetypeOptions"
:multiple="false"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="false"
placeholder="Pick a RR-Type..."
:preselect-first="false"
:max-height="500"
:taggable="false"
></VueMultiselect>
<div class="form-text">
{{ $t("rrtypeDescription") }}
</div>
</div>
</template>
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div>
<div class="my-3">
<label for="maxRetries" class="form-label">{{ $t("Retries") }}</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
{{ $t("retriesDescription") }}
</div>
</div>
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
{{ $t("ignoreTLSError") }}
</label>
</div>
<div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down">
{{ $t("Upside Down Mode") }}
</label>
<div class="form-text">
{{ $t("upsideDownModeDescription") }}
</div>
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
{{ $t("maxRedirectDescription") }}
</div>
</div>
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">{{ $t("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">
{{ $t("acceptedStatusCodesDescription") }}
</div>
</div>
</template>
<div class="mt-5 mb-1">
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div>
</div>
</div>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div>
<div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<div v-if="monitor.type === 'port' " class="mb-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>
<h2 class="mb-2">{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div class="mb-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 v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<div class="mb-3">
<label for="maxRetries" class="form-label">Retries</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
Maximum retries before the service is marked as down and a notification is sent
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</label>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
{{ $t("Setup Notification") }}
</button>
</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>
</form>
<div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">
Not available, please setup.
</p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</label>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
</div>
<NotificationDialog ref="notificationDialog" />
</div>
</form>
<NotificationDialog ref="notificationDialog" />
</transition>
</template>
<script>
import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
import { isDev } from "../util.ts";
const toast = useToast()
export default {
components: {
NotificationDialog,
VueMultiselect,
},
data() {
return {
processing: false,
monitor: {
notificationIDList: {},
// Do not add default value here, please check init() method
},
acceptedStatusCodeOptions: [],
dnsresolvetypeOptions: [],
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
ipRegexPattern: "((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))",
}
},
computed: {
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
if (! isDev) {
return this.ipRegexPattern;
}
return null;
},
pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit"
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
},
isAdd() {
return this.$route.path === "/add";
@@ -150,12 +243,40 @@ export default {
},
},
watch: {
"$route.fullPath" () {
"$route.fullPath"() {
this.init();
},
},
mounted() {
this.init();
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
let dnsresolvetypeOptions = [
"A",
"AAAA",
"CAA",
"CNAME",
"MX",
"NS",
"PTR",
"SOA",
"SRV",
"TXT",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
},
methods: {
init() {
@@ -170,6 +291,10 @@ export default {
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
}
} else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@@ -209,6 +334,40 @@ export default {
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
@import "../assets/vars.scss";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: 50rem;
background: $primary !important;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
}
</style>
<style scoped>
.shadow-box {
padding: 20px;

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

@@ -0,0 +1,16 @@
<template>
<transition name="slide-fade" appear>
<MonitorList />
</transition>
</template>
<script>
import MonitorList from "../components/MonitorList.vue";
export default {
components: {
MonitorList,
},
}
</script>

View File

@@ -1,104 +1,221 @@
<template>
<h1 class="mb-3">
Settings
</h1>
<transition name="slide-fade" appear>
<div>
<h1 v-show="show" class="mb-3">
{{ $t("Settings") }}
</h1>
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2>General</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3">
<label for="timezone" class="form-label">Timezone</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">
Auto: {{ guessTimezone }}
</option>
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
{{ timezone.name }}
</option>
</select>
</div>
<div class="shadow-box">
<div class="row">
<div class="col-md-6">
<h2 class="mb-2">{{ $t("Appearance") }}</h2>
<div>
<button class="btn btn-primary" type="submit">
Save
</button>
</div>
</form>
<div class="mb-3">
<label for="language" class="form-label">{{ $t("Language") }}</label>
<select id="language" v-model="$i18n.locale" class="form-select">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
{{ $i18n.messages[lang].languageName }}
</option>
</select>
</div>
<template v-if="loaded">
<template v-if="! settings.disableAuth">
<h2>Change Password</h2>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<label for="timezone" class="form-label">{{ $t("Theme") }}</label>
<div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
<label class="btn btn-outline-primary" for="btncheck1">{{ $t("Light") }}</label>
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
<label class="btn btn-outline-primary" for="btncheck2">{{ $t("Dark") }}</label>
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
<label class="btn btn-outline-primary" for="btncheck3">{{ $t("Auto") }}</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label>
<div>
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
<label class="btn btn-outline-primary" for="btncheck4">{{ $t("Normal") }}</label>
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
<label class="btn btn-outline-primary" for="btncheck5">{{ $t("Bottom") }}</label>
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
<label class="btn btn-outline-primary" for="btncheck6">{{ $t("None") }}</label>
</div>
</div>
</div>
<h2 class="mt-5 mb-2">{{ $t("General") }}</h2>
<form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3">
<label for="current-password" class="form-label">Current Password</label>
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
<label for="timezone" class="form-label">{{ $t("Timezone") }}</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">
{{ $t("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 for="new-password" class="form-label">New Password</label>
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
</div>
<label class="form-label">{{ $t("Search Engine Visibility") }}</label>
<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 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">
{{ $t("Allow indexing") }}
</label>
</div>
<div class="form-check">
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
<label class="form-check-label" for="searchEngineIndexNo">
{{ $t("Discourage search engines from indexing site") }}
</label>
</div>
</div>
<div>
<button class="btn btn-primary" type="submit">
Update Password
{{ $t("Save") }}
</button>
</div>
</form>
</template>
<h2>Advanced</h2>
<template v-if="loaded">
<template v-if="! settings.disableAuth">
<h2 class="mt-5 mb-2">{{ $t("Change Password") }}</h2>
<form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3">
<label for="current-password" class="form-label">{{ $t("Current Password") }}</label>
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
</div>
<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 class="mb-3">
<label for="new-password" class="form-label">{{ $t("New Password") }}</label>
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
</div>
<div class="mb-3">
<label for="repeat-new-password" class="form-label">{{ $t("Repeat New Password") }}</label>
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
<div class="invalid-feedback">
{{ $t("passwordNotMatchMsg") }}
</div>
</div>
<div>
<button class="btn btn-primary" type="submit">
{{ $t("Update Password") }}
</button>
</div>
</form>
</template>
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
</div>
</template>
</div>
<div class="notification-list col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<h2>{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<p v-else>
{{ $t("notificationDescription") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
{{ $t("Setup Notification") }}
</button>
</div>
</div>
</div>
<footer>
<div class="container-fluid">
Uptime Kuma -
{{ $t("Version") }}: {{ $root.info.version }} -
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
</div>
</footer>
<NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<template v-if="$i18n.locale === 'en' ">
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</template>
</div>
<div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" />
<template v-if="$i18n.locale === 'es-ES' ">
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
<p>Por favor usar con cuidado.</p>
</template>
<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>
<template v-if="$i18n.locale === 'zh-HK' ">
<p>你是否確認<strong>取消登入認証</strong></p>
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家例如 Cloudflare Access</p>
<p>請小心使用</p>
</template>
<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>
<template v-if="$i18n.locale === 'zh-CN' ">
<p>是否确定 <strong>取消登录验证</strong></p>
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能 Cloudflare Access</p>
<p>请谨慎使用</p>
</template>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
Setup Notification
</button>
</div>
<template v-if="$i18n.locale === 'de-DE' ">
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
<p>Bitte mit Vorsicht nutzen.</p>
</template>
<template v-if="$i18n.locale === 'sr' ">
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
<p>Молим Вас користите ово са пажњом.</p>
</template>
<template v-if="$i18n.locale === 'sr-latn' ">
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
<p>Molim Vas koristite ovo sa pažnjom.</p>
</template>
<template v-if="$i18n.locale === 'ko-KR' ">
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
<p> 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong> Uptime Kuma 앞에 사용자를 위한 기능이에요.</p>
<p>신중하게 사용하세요.</p>
</template>
</Confirm>
</div>
</div>
<NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</Confirm>
</transition>
</template>
<script>
@@ -123,7 +240,7 @@ export default {
return {
timezoneList: timezoneList(),
guessTimezone: dayjs.tz.guess(),
show: true,
invalidPassword: false,
password: {
currentPassword: "",
@@ -140,6 +257,10 @@ export default {
"password.repeatNewPassword"() {
this.invalidPassword = false;
},
"$i18n.locale"() {
localStorage.locale = this.$i18n.locale;
},
},
mounted() {
@@ -150,7 +271,7 @@ export default {
saveGeneral() {
localStorage.timezone = this.$root.userTimezone;
toast.success("Saved.")
this.saveSettings();
},
savePassword() {
@@ -171,6 +292,11 @@ export default {
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
if (this.settings.searchEngineIndex === undefined) {
this.settings.searchEngineIndex = false;
}
this.loaded = true;
})
},
@@ -201,8 +327,37 @@ export default {
}
</script>
<style scoped>
.shadow-box {
padding: 20px;
<style lang="scss" scoped>
@import "../assets/vars.scss";
.shadow-box {
padding: 20px;
}
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #fff;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #000;
}
}
footer {
color: #aaa;
font-size: 13px;
margin-top: 20px;
padding-bottom: 30px;
text-align: center;
}
</style>

View File

@@ -10,22 +10,22 @@
</div>
<p class="mt-3">
Create your admin account
{{ $t("Create your admin account") }}
</p>
<div class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
<label for="floatingInput">Username</label>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
<label for="floatingPassword">Password</label>
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div class="form-floating mt-3">
<input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
<label for="repeat">Repeat Password</label>
<label for="repeat">{{ $t("Repeat Password") }}</label>
</div>
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">

View File

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

View File

@@ -3,11 +3,16 @@
// Frontend uses util.ts
// Need to run "tsc" to compile if there are any changes.
import * as _dayjs from "dayjs";
const dayjs = _dayjs;
export const isDev = process.env.NODE_ENV === "development";
export const appName = "Uptime Kuma";
export const DOWN = 0;
export const UP = 1;
export const PENDING = 2;
export function flipStatus(s) {
export function flipStatus(s: number) {
if (s === UP) {
return DOWN;
}
@@ -19,7 +24,7 @@ export function flipStatus(s) {
return s;
}
export function sleep(ms) {
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -27,8 +32,8 @@ export function sleep(ms) {
* PHP's ucfirst
* @param str
*/
export function ucfirst(str) {
if (! str) {
export function ucfirst(str: string) {
if (!str) {
return str;
}
@@ -36,8 +41,69 @@ export function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1);
}
export function debug(msg) {
if (process.env.NODE_ENV === "development") {
console.log(msg)
export function debug(msg: any) {
if (isDev) {
console.log(msg);
}
}
declare global { interface String { replaceAll(str: string, newStr: string): string; } }
export function polyfill() {
/**
* String.prototype.replaceAll() polyfill
* 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: string, newStr: string) {
// 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 {
startTime: number;
constructor() {
this.startTime = dayjs().valueOf();
}
print(name: string) {
if (isDev) {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
}
}
}
/**
* Returns a random number between min (inclusive) and max (exclusive)
*/
export function getRandomArbitrary(min: number, max: number) {
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: number, max: number) {
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

Some files were not shown because too many files have changed in this diff Show More