Compare commits

..

440 Commits
1.5.3 ... 1.7.3

Author SHA1 Message Date
LouisLam
8d8d5987e7 update to 1.7.3 2021-09-30 14:44:37 +08:00
LouisLam
1fa90bffaa freeze vite version to 2.5.* (2.6.* is broken when build the project) 2021-09-30 14:43:29 +08:00
LouisLam
67f221d3c7 update to 1.7.2 2021-09-30 13:04:49 +08:00
LouisLam
6eceb4c744 Merge remote-tracking branch 'origin/master' 2021-09-29 17:20:58 +08:00
LouisLam
3e4154dfb5 Fix retry interval affected bug 2021-09-29 17:20:35 +08:00
Louis Lam
fbc8828ddc Merge pull request #464 from chakflying/fix-delete-monitor
Fix: clear important beats after deleteMonitor
2021-09-29 15:39:57 +08:00
LouisLam
3677aa639f update to 1.7.1 2021-09-29 14:26:30 +08:00
LouisLam
aaddfa1786 update package-lock.json 2021-09-29 13:30:28 +08:00
LouisLam
6d65d248f4 Merge branch 'bulgarian'
# Conflicts:
#	src/languages/da-DK.js
#	src/languages/fr-FR.js
#	src/languages/it-IT.js
#	src/languages/pl.js
#	src/languages/ru-RU.js
#	src/languages/zh-CN.js
2021-09-29 13:28:43 +08:00
LouisLam
87a4748b40 check back to root user for Debian, until problem has been found (#488) 2021-09-29 12:40:28 +08:00
Louis Lam
182bdf13a7 Merge pull request #500 from Saibamen/remove_timezone_console_log
Remove "Skip Timezone" console log from frontend
2021-09-29 12:16:51 +08:00
MrEddX
0f66e5cfc5 Added Bulgarian Language 2021-09-28 23:21:59 +03:00
Louis Lam
fe5ae46013 Merge pull request #480 from gaby/init-support
Add dumb-init to avoid zombie processes
2021-09-28 21:58:26 +08:00
Adam Stachowicz
756f317f82 Remove "Skip Timezone" console log from frontend 2021-09-28 07:29:32 +02:00
Louis Lam
cc9fe26584 Merge pull request #498 from kry008/patch-2
Update pl.js
2021-09-28 11:59:25 +08:00
kry008
99818aa370 Update pl.js
Aby było że to serwer sprawdza, a nie użytkownik ma.
Dokończenie tłumaczenia
2021-09-27 21:58:18 +02:00
Louis Lam
682265fe9c Merge pull request #481 from DX37/translation-ru
fix some ru-RU lines and add new ones
2021-09-28 00:13:37 +08:00
Louis Lam
4406e51ab6 Merge pull request #496 from bertyhell/bugfix/remove-zero-width-spaces
fix(nl-NL): remove ZWSP from translation
2021-09-28 00:13:16 +08:00
Louis Lam
c3d5be5a5e Merge pull request #493 from iomataani/master
updated translation
2021-09-28 00:08:12 +08:00
Louis Lam
dfe12c99c1 Merge pull request #490 from Minvinea/master
Update fr-FR.js
2021-09-28 00:07:58 +08:00
Louis Lam
bb69851160 Merge pull request #485 from Lrss/patch-1
Update Danish translation
2021-09-28 00:07:44 +08:00
Louis Lam
6c9323351d Merge pull request #484 from xinac721/master
Update Simplified Chinese Language(更新简体中文语言)
2021-09-28 00:06:57 +08:00
Bert Verhelst
8e9fa20e57 fix(nl-NL): remove ZWSP from translation 2021-09-27 17:51:48 +02:00
Ioma Taani
da7c29f4b9 updated translation 2021-09-27 13:23:53 +02:00
Minvinea
e545d48583 Update fr-FR.js
Update line 112 to 181
2021-09-26 21:39:38 +02:00
Lars Sørensen
8d84d8f891 Update da-DK.js with new strings 2021-09-26 15:28:46 +02:00
新逸Cary
74acc2cea7 Update Simplified Chinese Language(更新简体中文语言) 2021-09-26 14:31:16 +08:00
新逸Cary
ba4a4089eb Merge pull request #3 from louislam/master
update-21.09.26
2021-09-26 13:17:48 +08:00
DX37
122631c91b fix some ru-RU lines and add new ones 2021-09-26 01:44:00 +07:00
Juan Calderon-Perez
1e12f25c4b Add support for using init to start service 2021-09-25 12:26:30 -04:00
LouisLam
9e10296290 Merge remote-tracking branch 'origin/master' 2021-09-25 22:44:47 +08:00
LouisLam
87e213085f add /status, alias of /status-page (#471) 2021-09-25 22:44:29 +08:00
Louis Lam
d9038f1da2 Merge pull request #473 from Ponkhy/german-language
Added german translation for the status page
2021-09-24 23:26:30 +08:00
Louis Lam
3fb2d0ce68 Update README.md 2021-09-24 22:38:54 +08:00
Ponkhy
50a33e3b45 Added german translation for the status page 2021-09-24 15:56:35 +02:00
LouisLam
ce9a97a107 update to 1.7.0 2021-09-24 19:17:29 +08:00
LouisLam
0afa3a2c21 Merge branch '1.6.X'
# Conflicts:
#	server/database.js
#	server/server.js
2021-09-24 19:12:57 +08:00
LouisLam
51512b6f5f update dependencies 2021-09-24 19:00:06 +08:00
Louis Lam
3d79e841c9 Merge pull request #466 from chakflying/public-dashboard
[status-page] hide select if no monitors to add
2021-09-24 18:33:00 +08:00
Nelson Chan
d2af42e579 Fix: Add placeholder if no monitors to add 2021-09-24 16:51:53 +08:00
LouisLam
d6e3bd2282 Merge remote-tracking branch 'origin/master' 2021-09-24 16:01:18 +08:00
LouisLam
70fbe9a2d9 fix alpine nightly build 2021-09-24 16:00:08 +08:00
Louis Lam
c669e7eaba Merge pull request #469 from chakflying/patch-1
Fix: Fix importing tag if tag doesn't exist
2021-09-24 15:41:13 +08:00
LouisLam
9a9fd41f62 Merge branch 'master' into patch-1 2021-09-24 15:33:13 +08:00
LouisLam
50b868e751 fix #465 2021-09-24 15:26:48 +08:00
LouisLam
a856780066 fix the active link problem 2021-09-24 15:00:52 +08:00
LouisLam
266b03fbf7 language key from "Status Page Nothing" to "statusPageNothing" 2021-09-24 14:42:54 +08:00
Nelson Chan
662c97dcde Fix: Fix importing tag if tag doesn't exist 2021-09-24 14:34:53 +08:00
Louis Lam
1ebf752f1a Merge pull request #415 from Ponkhy/import-export
Added import/export compatibility for version 1.7
2021-09-24 14:00:28 +08:00
Mawoka
69bb6ef290 Removed empty selector if no monitor available 2021-09-24 00:19:09 +08:00
Ponkhy
720ea850e1 Added space between Dashboard and settings button 2021-09-23 17:33:23 +02:00
Ponkhy
7fb55b8875 Fixed issues 2021-09-23 17:31:01 +02:00
Ponkhy
4786514e9f Merge branch 'louislam:master' into import-export 2021-09-23 17:24:53 +02:00
Nelson Chan
32c9dfbb31 Fix: clear important beats after deleteMonitor 2021-09-23 23:21:08 +08:00
Ponkhy
d3d4363031 Used compare-version instead of replace 2021-09-23 17:15:11 +02:00
Louis Lam
6f352a6e3c [guide] update how to start dev server 2021-09-23 22:48:19 +08:00
Louis Lam
3fb06a8ba4 Merge pull request #463 from CosmoAbdon/ptBR-Translation
add: BR Portuguese Language and new words
2021-09-23 21:04:39 +08:00
Louis Lam
b3a5a5b0ba Merge pull request #462 from Saibamen/development_server
`start-server-dev` command. Fixes #460
2021-09-23 21:04:23 +08:00
Cosmo Abdon
a4cad3db65 add: BR Portuguese Language and new words 2021-09-23 09:42:33 -03:00
Adam Stachowicz
5fa9b33c79 Install cross-env as dev dependency for now 2021-09-23 12:49:15 +02:00
Adam Stachowicz
f6a984b671 start-server-dev. Fixes #460 2021-09-23 12:45:30 +02:00
LouisLam
23a63213aa Merge branch 'master' into import-export
# Conflicts:
#	server/server.js
2021-09-23 17:20:13 +08:00
Louis Lam
a9bb8ae6a1 Merge pull request #124 from chakflying/public-dashboard
Implement Public Dashboard
2021-09-23 16:54:07 +08:00
LouisLam
27d4c3c194 [status page] improve mobile layout 2021-09-23 16:31:45 +08:00
LouisLam
439f45d91e [status page] improve the entry 2021-09-23 13:57:24 +08:00
Louis Lam
66598a81cc Merge pull request #452 from chakflying/patch-8
Fix: Fix TCP Ping hostname Regex
2021-09-23 12:49:56 +08:00
Nelson Chan
45a6f364e1 Chore: Fix backslash in IP regex 2021-09-22 23:05:46 +08:00
Nelson Chan
95391be2ab Revert Regex update 2021-09-22 22:36:27 +08:00
LouisLam
5f533b9091 [status page] fix wrong foreign key 2021-09-22 18:50:20 +08:00
LouisLam
29920f6b60 [status page] use normal link to status-page, due to data reset problem 2021-09-22 18:05:40 +08:00
LouisLam
ad1bb93730 [status page] fix logo url 2021-09-22 15:31:15 +08:00
LouisLam
0a5a6e6a4b [status page] fix monitor order 2021-09-22 15:23:58 +08:00
LouisLam
fe0fc63843 [status page] send uptime 2021-09-22 15:10:08 +08:00
Nelson Chan
5b2e1f6086 Fix: Allow number ending TLD 2021-09-22 05:29:56 +08:00
LouisLam
8c7ee94769 add modified apicache library with disabling client cache 2021-09-22 00:58:22 +08:00
LouisLam
0834770509 [status page] also apply the title to <title> 2021-09-21 23:59:46 +08:00
LouisLam
a71814c80b add util.ts comments 2021-09-21 21:28:45 +08:00
LouisLam
17073fd786 Merge branch 'master' into public-dashboard 2021-09-21 21:25:16 +08:00
LouisLam
15c00d9158 upload logo and expose ./data/upload to url 2021-09-21 21:22:35 +08:00
Nelson Chan
d2b34f9285 Chore: make eslint happy 2021-09-21 18:53:45 +08:00
Nelson Chan
a96a515087 Fix: Fix hostname with dash 2021-09-21 18:44:32 +08:00
Nelson Chan
2eab919ae0 Fix: Fix typo 2021-09-21 18:41:19 +08:00
Nelson Chan
c09b67b94d Fix: Fix TCP Ping hostname Regex 2021-09-21 18:27:40 +08:00
LouisLam
61c737c53c fix alert text color in dark mode 2021-09-21 17:24:46 +08:00
Louis Lam
2820118eba Merge pull request #446 from Saibamen/util
Add comments from `util.ts` to `util.js`
2021-09-21 17:23:12 +08:00
LouisLam
469e8f6fd6 Merge branch 'master' into public-dashboard
# Conflicts:
#	package-lock.json
2021-09-21 17:05:13 +08:00
LouisLam
780cb0a145 install-legacy-peer-deps to install-legacy 2021-09-21 17:03:37 +08:00
LouisLam
1c8b3ce451 no need to build sqlite and add nightly for alpine 2021-09-21 10:47:55 +08:00
Adam Stachowicz
e8ef63e0a3 removeComments to false 2021-09-21 00:48:26 +02:00
LouisLam
4591adc05e second attempt: prebuilt node-sqlite3 and update SQLite to 3.36 2021-09-21 01:15:20 +08:00
LouisLam
5f6aa32844 fix store/fetch status page config 2021-09-20 20:44:07 +08:00
LouisLam
a8e170f6a8 Merge branch 'master' into public-dashboard
# Conflicts:
#	server/server.js
2021-09-20 18:48:44 +08:00
LouisLam
6aaf984f29 fix retryInterval = 0 if it is an old monitor. (#380 https://github.com/louislam/uptime-kuma/pull/380#issuecomment-922618356) 2021-09-20 18:23:53 +08:00
LouisLam
76a39f1388 update to 1.6.3 2021-09-20 16:56:03 +08:00
LouisLam
34c0fa59a8 fix reset-password (#448)
(cherry picked from commit b0e9c5bcb4)
2021-09-20 16:33:22 +08:00
LouisLam
0664217a09 Merge remote-tracking branch 'origin/master' 2021-09-20 16:29:43 +08:00
LouisLam
b0e9c5bcb4 fix reset-password (#448) 2021-09-20 16:29:18 +08:00
LouisLam
0b572df3d0 [status page] store config 2021-09-20 16:22:18 +08:00
Adam Stachowicz
bcc02c1680 Add formatting and comments from util.ts to util.js 2021-09-19 23:09:52 +02:00
Louis Lam
795d5f586f Merge pull request #444 from Saibamen/avg-ping_avg-response
Avg. Ping and Avg. Response translation keys
2021-09-20 02:23:50 +08:00
LouisLam
7ee98d989c [status page] implement rest api for heartbeat 2021-09-19 23:24:51 +08:00
Adam Stachowicz
7dc1e84e44 Avg. Ping and Avg. Response translation keys 2021-09-19 15:15:12 +02:00
LouisLam
6350c43cc3 fix newline? 2021-09-19 19:20:27 +08:00
LouisLam
fd95d41d9f [status page] many update and save group list 2021-09-19 19:04:51 +08:00
Louis Lam
7665bae927 Merge pull request #441 from iomataani/master
Italian translation updated
2021-09-19 16:32:08 +08:00
Ioma Taani
09e38269c6 Merge branch 'louislam:master' into master 2021-09-19 09:45:42 +02:00
LouisLam
6681f49a58 Merge branch 'master' into public-dashboard
# Conflicts:
#	.dockerignore
2021-09-19 15:16:13 +08:00
Louis Lam
3fc2ba3d76 Merge pull request #436 from nbvcxz/running_non-root_user
Docker entrypoint to run the application as non-root user
2021-09-19 14:33:20 +08:00
Louis Lam
78e12ab899 Merge pull request #439 from gaby/dockerignore
Add missing entries to dockerignore
2021-09-19 12:58:55 +08:00
Juan Calderon-Perez
2b8c049e7b Add missing entries to dockerignore 2021-09-18 13:53:24 -04:00
LouisLam
f0ac3c82d2 add some comments 2021-09-19 00:51:05 +08:00
Louis Lam
803029a9e4 Merge pull request #429 from Ponkhy/main
Added regex to hostname input
2021-09-19 00:38:40 +08:00
Louis Lam
c3f1cebeab Merge pull request #435 from DX37/translation-ru
fresh translations to ru-RU.js and Settings.vue
2021-09-19 00:37:37 +08:00
Louis Lam
5d9814559c Merge pull request #424 from Saibamen/fix_PL
Fix polish translation and translate new keys
2021-09-19 00:37:23 +08:00
DX37
9ef45a9c7e fresh translations to ru-RU.js and Settings.vue 2021-09-18 22:25:06 +07:00
Michal Ciania
7f78cc8d0f Substitute default values only once 2021-09-18 11:33:25 +02:00
Ioma Taani
ca84044809 Merge branch 'louislam:master' into master 2021-09-17 23:05:56 +02:00
Michal Ciania
9eaa4ab846 Docker entrypoint for running the application as non-root user 2021-09-17 22:57:27 +02:00
Marcos Sigueros Fernández
c3122a9807 Update es-ES.js (#430)
Simple but annoying language fix.
2021-09-17 22:20:45 +08:00
Ponkhy
f6498caa9a Merge branch 'main' of https://github.com/Ponkhy/uptime-kuma into main 2021-09-17 16:02:28 +02:00
Ponkhy
3a0bc80016 Added regex to hostname input 2021-09-17 16:02:20 +02:00
Ioma Taani
cbbc503f8e Merge branch 'louislam:master' into master 2021-09-17 11:31:21 +02:00
Louis
5be51abd8f Merge remote-tracking branch 'origin/master' 2021-09-17 15:19:36 +08:00
Louis
6ec219908b test nodejs 16 2021-09-17 15:19:19 +08:00
Louis
a6fdd272a6 [status page] minor 2021-09-17 14:42:19 +08:00
Ponkhy
1b5e723f60 Added descriptions to uploadBackup function 2021-09-17 03:25:18 +02:00
Ponkhy
4bdada36a9 Removed if includes version 2021-09-16 20:18:31 +02:00
Adam Stachowicz
250c2bdd6d Missing this 2021-09-16 20:17:25 +02:00
Adam Stachowicz
8e49d84050 Fix polish translation and translate new keys 2021-09-16 20:13:57 +02:00
Louis Lam
112d72da47 update discussion 2021-09-17 00:48:04 +08:00
LouisLam
9b8f01cfc6 since eslint can auto fix semicolon, standardize all end with semicolon 2021-09-16 22:57:34 +08:00
LouisLam
579e07ded4 Merge branch 'master' into public-dashboard 2021-09-16 22:52:05 +08:00
kry008
a04878fa84 Finishing the translation of PL (#422) 2021-09-16 22:49:24 +08:00
LouisLam
2955abb5d9 [status page] create incident 2021-09-16 22:48:28 +08:00
LouisLam
8230cfe13f use "next" tag to keep vue3 up-to-update 2021-09-16 20:07:26 +08:00
Ponkhy
8b463e70df Apply suggestions from @Saibamen
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-16 12:29:33 +02:00
LouisLam
392f8275b3 Merge branch 'master' into public-dashboard
# Conflicts:
#	server/database.js
2021-09-16 16:53:54 +08:00
Ioma Taani
783ec97004 updated translations 2021-09-16 09:10:13 +02:00
Siim Ots
4f4fe39c9b Simple "Add to Homescreen" manifest (PWA) (#411)
* simple "add to homescreen" manifest

* remove ?source=pwa

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

* run TinyPNG

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Co-authored-by: LouisLam <louislam@users.noreply.github.com>
2021-09-16 14:42:56 +08:00
LouisLam
611d214a32 [status page] checkpoint 2021-09-16 14:37:57 +08:00
Louis Lam
7a0cebf5bb Merge pull request #414 from Saibamen/lang_fixes
Fix languages
2021-09-16 14:33:05 +08:00
Ponkhy
54aa68ec58 Added import/export compatibility for version 1.7 2021-09-15 22:07:28 +02:00
Adam Stachowicz
217637aa1f Add de-DE translation 2021-09-15 21:19:16 +02:00
Adam Stachowicz
ab22961538 More info about update-language-files command 2021-09-15 21:16:22 +02:00
Adam Stachowicz
8ba8de07ae Missing this 2021-09-15 20:59:29 +02:00
Adam Stachowicz
a34b2623c8 Remove double spaces in console.log output with multiple parameters 2021-09-15 20:57:46 +02:00
Adam Stachowicz
79920b5f2c Fix languages after 80322cbfe7
Fix ESLint deprecated warning. Update `README.md` for  languages. Update `update-language-files` script
2021-09-15 20:52:39 +02:00
Louis Lam
72783fd94c Merge pull request #380 from No0Vad/retry-heartbeat-interval
Added support for a retry interval to monitors
2021-09-16 00:21:53 +08:00
LouisLam
80322cbfe7 Merge branch 'import-export'
# Conflicts:
#	src/languages/it-IT.js
#	src/languages/nl-NL.js
2021-09-16 00:18:07 +08:00
LouisLam
7e0272077b cleanup demo mode code 2021-09-16 00:13:28 +08:00
LouisLam
b2ff6b6098 update ignore files and delete demo db 2021-09-16 00:12:39 +08:00
LouisLam
512ff09cca set entry page 2021-09-15 20:40:26 +08:00
LouisLam
e8f4fabcd0 [status page] crop and resize logo 2021-09-15 18:28:48 +08:00
Ponkhy
f679c1cbaf Export button align left 2021-09-15 12:01:30 +02:00
LouisLam
2ab06f87b8 Merge branch 'master' into public-dashboard 2021-09-15 16:46:05 +08:00
Louis Lam
76db55b657 Merge pull request #395 from WillianRod/feat/add-microsoft-teams-notification
Add microsoft teams notification provider
2021-09-15 16:38:57 +08:00
LouisLam
1693873f4a [Teams] change handleTestNotification to GeneralNotification 2021-09-15 16:38:28 +08:00
Louis Lam
53bfdb7dad Merge pull request #409 from iomataani/master
updated
2021-09-15 14:38:17 +08:00
LouisLam
db05b506f3 [status page] checkpoint 2021-09-15 14:34:30 +08:00
Ioma Taani
0752f810f1 updated 2021-09-15 08:33:30 +02:00
Louis Lam
200d233607 Merge pull request #407 from SametKUM/master
Turkish Language added
2021-09-15 11:08:25 +08:00
samet.kum
7274913686 Turkish Language added 2021-09-15 02:55:04 +03:00
No0Vad
1300448bed Adjustments to the retry interval
The monitor logic for when to use "retryInterval" is updated. Also removed some texts when they are no longer needed.
2021-09-15 00:59:06 +02:00
Louis Lam
3f67326fce Merge pull request #404 from Ponkhy/main
Added search key
2021-09-15 01:20:51 +08:00
Louis Lam
8b48f9bd22 Merge pull request #405 from zidingz/patch-1
Update SECURITY.md
2021-09-15 01:19:29 +08:00
Louis Lam
76348cae5d Update SECURITY.md 2021-09-15 01:17:33 +08:00
Louis Lam
0b32adadb8 Update SECURITY.md 2021-09-15 01:12:59 +08:00
Ziding Zhang
3425a27a0e Update SECURITY.md
I'd like to report a security issue and felt it most prudent not to disclose it publicly, as per your Security policy instructions.

If not a hassle, might you kindly add an email, or other private contact method? GitHub [recommends](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository) this best practice to ensure security issues are responsibly disclosed, and it would serve as a simple instruction for security researchers in the future.

Thank you for your consideration!
2021-09-14 17:54:51 +01:00
Ponkhy
fff5fd8e9e Added search key 2021-09-14 17:48:29 +02:00
LouisLam
1d6670ed9a Merge branch 'master' into public-dashboard 2021-09-14 23:29:22 +08:00
LouisLam
795411a11c Merge remote-tracking branch 'origin/master' 2021-09-14 23:28:52 +08:00
LouisLam
3234aec5b3 NODE_ENV if not set, change to production 2021-09-14 23:28:38 +08:00
LouisLam
afe91078c4 [status page] checkpoint 2021-09-14 23:27:11 +08:00
Louis Lam
2009f7097d Merge pull request #403 from Ponkhy/main
Added german translation for tags manager
2021-09-14 23:21:40 +08:00
Ponkhy
c14b71a4a7 Added german translation for tags manager 2021-09-14 17:05:17 +02:00
LouisLam
35fe9690e8 update to 1.6.2 2021-09-14 19:34:54 +08:00
LouisLam
ad2062713c fix pi4 on docker 2021-09-14 19:04:56 +08:00
Louis Lam
379656335d Merge pull request #402 from xoniq/lang-nl
Update new / updated translations to nl_NL
2021-09-14 15:49:26 +08:00
LouisLam
fc9b4617ca link interval and retryInterval 2021-09-14 15:48:25 +08:00
Jelle Posthuma
40d301457a Update new / updated translations to nl_NL 2021-09-14 09:22:50 +02:00
LouisLam
9902c181bc Merge branch 'master' into public-dashboard
# Conflicts:
#	src/components/MonitorList.vue
#	src/icon.js
2021-09-14 15:05:04 +08:00
Louis Lam
069c811af8 Merge pull request #278 from chakflying/tags
Monitor: Tags with metadata
2021-09-14 14:57:53 +08:00
LouisLam
f9311e4e7f [status page] 2021-09-14 14:55:45 +08:00
LouisLam
34abff4724 manual merge master 2021-09-14 14:19:23 +08:00
LouisLam
d7a230ac15 Merge branch 'master' into public-dashboard
# Conflicts:
#	server/server.js
#	src/main.js
2021-09-14 14:16:24 +08:00
LouisLam
9da2a01a74 [status page] checkpoint 2021-09-14 14:12:27 +08:00
Nelson Chan
74da02da2c Fix: Prevent enter in modal from submitting form 2021-09-14 12:25:31 +08:00
LouisLam
97360dab26 add /.well-known/change-password 2021-09-14 12:10:25 +08:00
Louis Lam
56dad006b5 Merge pull request #401 from NeuralMiner/patch-1
Allow SMTP that doesnt require authentication.
2021-09-14 10:05:43 +08:00
NeuralMiner
47092258f8 Allow SMTP that doesnt require authentication. 2021-09-13 16:36:37 -06:00
Willian Rodrigues Barbosa
a0838543b9 Merge branch 'feat/add-microsoft-teams-notification' of https://github.com/WillianRod/uptime-kuma into feat/add-microsoft-teams-notification 2021-09-13 14:03:14 -03:00
Willian Rodrigues Barbosa
ccb8736b3d fix: send msg if heartbeat message is not set 2021-09-13 14:02:52 -03:00
LouisLam
f351b423c5 link to the english version of microsoft link 2021-09-13 20:25:41 +08:00
LouisLam
ab6b97d77a update dependencies, fix axios security problem 2021-09-13 20:14:50 +08:00
LouisLam
0e16e88fa8 Merge remote-tracking branch 'origin/master' 2021-09-13 20:06:24 +08:00
LouisLam
c746923018 merge italian language file 2021-09-13 20:05:48 +08:00
LouisLam
df8754827d Merge branch 'italian-language-update'
# Conflicts:
#	src/languages/it-IT.js
2021-09-13 20:04:46 +08:00
Louis Lam
2c02dad1f9 Merge pull request #393 from Revyn112/master
add possibility to have a prefixMessage in discord notification before the embed
2021-09-13 20:02:55 +08:00
LouisLam
600ec6d171 Merge branch 'master' into i18n_router_own_files 2021-09-13 19:58:14 +08:00
LouisLam
728636de06 Merge branch 'feature/timezones-package' 2021-09-13 19:56:57 +08:00
LouisLam
8be7fd5c45 Merge remote-tracking branch 'bertyhell/feature/timezones-package' 2021-09-13 19:56:29 +08:00
LouisLam
59ff9fa293 update missing timezone console msg 2021-09-13 19:53:29 +08:00
LouisLam
a654e409df Merge branch 'master' into feature/timezones-package 2021-09-13 19:48:42 +08:00
LouisLam
76f1f34a0a fix #397, incorrect encode function for export file 2021-09-13 19:25:12 +08:00
LouisLam
35aca46b68 [status page] checkpoint 2021-09-13 19:21:39 +08:00
Paride Barison
6a925c7ed4 sistemato l'ordine 2021-09-13 09:19:07 +02:00
Paride Barison
8b9b86b478 updated 2021-09-13 09:10:42 +02:00
Nelson Chan
7c4b5f189b Fix: Fix hiding form when reusing draft tags 2021-09-13 14:12:17 +08:00
Nelson Chan
a6f9762c4d Feat: Split add tag to modal
Fix: Fix wrong css class

Fix: More misc. styling fixes
2021-09-13 14:12:17 +08:00
Nelson Chan
a0e4e96160 Fix: Fix tag removal, reuse and validation 2021-09-13 14:12:17 +08:00
Nelson Chan
fcbeed55bf Fix: Allow removing and re-adding the same tag
like, why would you even do this?
2021-09-13 14:12:17 +08:00
Nelson Chan
9b5abf9bb1 Fix: reset editMonitor after save 2021-09-13 14:12:17 +08:00
Nelson Chan
73f4b8e177 Fix: Improve various tags validation logic 2021-09-13 14:12:17 +08:00
Nelson Chan
e6669fbb9e Fix: Improve searchbar padding on mobile 2021-09-13 14:12:17 +08:00
Nelson Chan
e66a2d362d Feat: Add monitorList search box 2021-09-13 14:12:17 +08:00
Nelson Chan
c2148fc0f1 Fix: Slightly reduce tag opacity on light theme 2021-09-13 14:12:16 +08:00
Nelson Chan
6e3a904aaa WIP: Add tags functionality
WIP: add color column, show tags

WIP: Improve TagsManager styling & workflow

WIP: Improve styling & validation, use translation

WIP: Complete TagsManager functionality

WIP: Add tags display in monitorList & Details

Fix: update tags list after edit

Fix: slightly improve tags styling

Fix: Improve mobile UI

Fix: Fix tags not showing on create monitor

Fix: bring existingTags inside tagsManager

Fix: remove unused tags prop

Fix: Fix formatting, bump db version
2021-09-13 14:12:11 +08:00
Louis Lam
50175b733c Merge pull request #396 from Ponkhy/2fa
Hide 2FA section if disabled auth
2021-09-13 09:16:09 +08:00
No0Vad
2617e1f4d8 Update database.js 2021-09-13 00:25:18 +02:00
No0Vad
91ee39ec60 Merge branch 'master' into retry-heartbeat-interval 2021-09-13 00:19:51 +02:00
Ponkhy
4711aeb355 Merge branch 'master' into import-export 2021-09-12 23:21:33 +02:00
Ponkhy
db9e97d859 Merge pull request #2 from Saibamen/import-export
Separate import and export. Fix ESLint
2021-09-12 23:20:16 +02:00
Adam Stachowicz
7199bb5ead Remove unused i18n key 2021-09-12 23:11:07 +02:00
Ponkhy
bb87972543 Hide 2FA section if disabled auth 2021-09-12 21:58:52 +02:00
Adam Stachowicz
ea723c7595 Separate import and export. Fix ESLint 2021-09-12 21:35:37 +02:00
LouisLam
e205adfd7b [status page] developing 2021-09-13 02:26:45 +08:00
Willian Rodrigues Barbosa
063d64eec8 feat: add microsoft teams notification provider 2021-09-12 14:46:59 -03:00
Adam Stachowicz
5abf2e7597 Missing this 2021-09-12 19:44:33 +02:00
Adam Stachowicz
7c83bed273 Remove double spaces in Dockerfiles 2021-09-12 19:43:14 +02:00
Adam Stachowicz
1cc788da68 Missing this 2021-09-12 19:39:28 +02:00
Adam Stachowicz
54ffef8b7a Own files for i18n and router + fix some of markdown warnings 2021-09-12 19:23:51 +02:00
Revyn112
44aa837a9d Update NotificationDialog.vue 2021-09-12 19:17:09 +02:00
LouisLam
f47f7758f9 Merge branch 'master' into public-dashboard
# Conflicts:
#	server/database.js
#	server/server.js
#	server/util-server.js
2021-09-13 01:09:01 +08:00
Bert Verhelst
ddcd3c2a19 Merge remote-tracking branch 'origin/master' into feature/timezones-package 2021-09-12 19:03:05 +02:00
LouisLam
7df9698e5d eslint: comma-dangle for language files 2021-09-13 00:58:45 +08:00
Bert Verhelst
471333ad03 fix(util-frontend): revert typescript conversion 2021-09-12 18:56:39 +02:00
Ponkhy
d313966d80 Merge branch 'master' into import-export 2021-09-12 18:46:11 +02:00
LouisLam
8205f90f3d update language files 2021-09-13 00:45:43 +08:00
LouisLam
02247c4174 Merge remote-tracking branch 'origin/master' 2021-09-13 00:38:17 +08:00
Denis Freund
8352d9abbe add posibility to have a prefixMessage before the embed 2021-09-12 17:54:12 +02:00
Louis Lam
2d7767c905 Merge pull request #392 from bertyhell/bugfix/button-margin-settings
fix(settings): add some button bottom margin for narrow screens
2021-09-12 23:49:21 +08:00
Louis Lam
c52b9b63d5 Merge pull request #389 from Ponkhy/default-notification
Wording correction for notification modal
2021-09-12 23:49:09 +08:00
Louis Lam
e55193270d Merge pull request #377 from Lrss/master
Fix: Inconsistent white-space for the "Avg."-Ping/Response title.
2021-09-12 23:48:29 +08:00
Louis Lam
9d39071a91 Merge pull request #363 from Ponkhy/2fa
Added simple TOTP Two Factor Authentication
2021-09-12 23:47:42 +08:00
No0Vad
389d247463 Update server/database.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-12 17:05:23 +02:00
Bert Verhelst
a170694a83 fix(util-frontend): improve formatting timezoneList
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-12 15:21:40 +02:00
Bert Verhelst
5969e913b5 fix(util-frontend): improve formatting
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-12 15:19:27 +02:00
Louis Lam
0d280f5edb Merge pull request #376 from chakflying/patch-7
Fix: Fix events table width on mobile
2021-09-12 21:12:56 +08:00
Bert Verhelst
660b969178 fix(settings): switch custom styles to bootstrap class for bottom margin 2021-09-12 14:52:47 +02:00
Bert Verhelst
c26622aa39 fix(settings): add some button bottom margin for narrow screens 2021-09-12 13:40:07 +02:00
Bert Verhelst
a7674755ba Move timezones to npm package and convert util-frontend.js to typescript 2021-09-12 13:27:41 +02:00
Louis Lam
6d7cb44acc Merge pull request #387 from jtagcat/et2
i10n: update Estonian
2021-09-12 14:03:02 +08:00
LouisLam
400fc13cf2 add npm install script for npm7 2021-09-12 12:39:22 +08:00
Ponkhy
db14768229 Wording correction for notification modal 2021-09-12 01:44:05 +02:00
No0Vad
3d27561e5a Update en.js
Fixed spelling error
2021-09-12 00:24:33 +02:00
jtagcat
b80c88d9a0 i10n: update Estonian 2021-09-12 00:51:03 +03:00
Ponkhy
862672731f Minor typo fix 2021-09-11 22:06:26 +02:00
Ponkhy
7fee4a7ea7 Added import options 2021-09-11 21:53:17 +02:00
LouisLam
c4f78d776e [2fa] "UptimeKuma" to "Uptime Kuma" 2021-09-12 02:25:51 +08:00
No0Vad
f8f9f59464 Added support for a retry interval to monitors
If a check fails and retries are used you can now specify a specific value for that. So you can check faster if the site goes back up again.
2021-09-11 18:54:55 +02:00
LouisLam
934685637a [Status Page] WIP: Checkpoint 2021-09-11 23:43:07 +08:00
Ponkhy
295ccba44b Adjusted for new db patch management 2021-09-11 16:37:33 +02:00
Ponkhy
8cd5bad44c Merge branch 'master' into 2fa 2021-09-11 16:32:11 +02:00
Ponkhy
d003abcd60 Update src/languages/en.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-11 14:34:23 +02:00
Ponkhy
f6d1a82989 Update server/server.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-11 14:34:19 +02:00
Ponkhy
651b525d06 Update server/server.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-11 14:34:12 +02:00
Lars Sørensen
66769e1c79 Fixed inconsistent white-space for the "Avg."-Ping/Response title. 2021-09-11 13:52:41 +02:00
LouisLam
3e25f0e9d9 [Status Page] WIP: Checkpoint 2021-09-11 19:40:03 +08:00
Lars Sørensen
39c7b5e748 Added new Strings to Danish translation 2021-09-11 13:37:53 +02:00
Lars Sørensen
b709857e19 Better wording for Danish language 2021-09-11 13:20:40 +02:00
Nelson Chan
9e10f7d35f Fix: Fix events table width on mobile 2021-09-11 18:56:27 +08:00
Louis Lam
6caae725f9 Merge pull request #375 from Saibamen/format_backup_json
Format JSON file before download
2021-09-11 16:57:33 +08:00
Adam Stachowicz
c387fb264e Format JSON file before download 2021-09-11 10:50:56 +02:00
LouisLam
4b0a8087a2 do not connect to socket io for status page 2021-09-11 16:22:30 +08:00
LouisLam
08c7a37052 Merge branch 'master' into public-dashboard 2021-09-11 15:09:08 +08:00
LouisLam
0969be5981 Revert "WIP: Add login page, nav bar buttons"
This reverts commit b9a6088f50.
2021-09-11 15:08:56 +08:00
LouisLam
2ed8f12dc0 Merge remote-tracking branch 'origin/master' 2021-09-11 14:46:04 +08:00
LouisLam
2da77d8448 fix ipv6 connection problem 2021-09-11 14:37:05 +08:00
Louis Lam
f8322de5da Update bug_report.md 2021-09-11 13:44:06 +08:00
Louis Lam
e21a18a593 Update ask-for-help.md 2021-09-11 13:41:22 +08:00
Ponkhy
14b7688b70 Added NOT NULL to twofa_status 2021-09-11 00:13:05 +02:00
LouisLam
d953ba7c60 update to 1.6.1 2021-09-11 03:12:25 +08:00
LouisLam
ef1604675b change to node:14-bullseye-slim, and reduce the docker image size 2021-09-11 02:50:29 +08:00
Louis Lam
83fc844e8e Update deployment.yml 2021-09-10 22:32:11 +08:00
Louis Lam
22a7acbaf6 Update README.md 2021-09-10 22:03:28 +08:00
LouisLam
201bba63d7 add a healthcheck comment 2021-09-10 21:17:20 +08:00
Ioma Taani
0d6888c586 Merge branch 'louislam:master' into italian-language-update 2021-09-10 13:36:43 +02:00
Ioma Taani
a06fc14213 Merge branch 'louislam:master' into italian-language-update 2021-09-10 12:08:29 +02:00
LouisLam
31b4a5c33e build-docker, debian first 2021-09-10 18:08:23 +08:00
Paride Barison
951ece8a65 updated 2021-09-10 12:07:54 +02:00
LouisLam
a49df29a87 update to 1.6.0 2021-09-10 17:56:33 +08:00
LouisLam
08de0090dc add a better approach for patching db, change setting.value from varchar to TEXT, restore Database.close() to 1.2.0 2021-09-10 17:23:45 +08:00
Louis Lam
97df200d6c Merge pull request #365 from Ponkhy/master
Minor translation fixes
2021-09-10 11:46:20 +08:00
Louis Lam
d5b17a3778 Merge pull request #367 from xinac721/master
update Simplified Chinese language(更新简体中文语言)
2021-09-10 11:45:57 +08:00
新逸Cary
2f0d994c83 update Simplified Chinese language(更新简体中文语言) 2021-09-10 09:26:01 +08:00
新逸Cary
e683033f4e Merge pull request #2 from louislam/master
update
2021-09-10 08:52:38 +08:00
Ponkhy
c3c576bd13 Added "Show URI" translation 2021-09-10 01:33:26 +02:00
Ponkhy
c62732fa9c Minor translation fixes 2021-09-10 01:24:21 +02:00
Ponkhy
d823493fad Added maxlengh to token verify input 2021-09-09 22:08:34 +02:00
Ponkhy
463c385cf1 Only show modal footer if uri was generated 2021-09-09 21:38:39 +02:00
Ponkhy
59cccf8c50 Fixed typo 2021-09-09 21:12:29 +02:00
Ponkhy
403202d4d4 Added simple TOTP Two Factor Authentication 2021-09-09 21:10:31 +02:00
Louis Lam
2583dbe0e0 Update README.md 2021-09-09 22:58:31 +08:00
LouisLam
ca479673e7 avoid Chrome duplicate id warning 2021-09-09 21:28:27 +08:00
LouisLam
573c7faddd switch on the notification, if it is added in EditMonitor.vue 2021-09-09 21:24:29 +08:00
LouisLam
7a4432de1e better switch button color in dark mode 2021-09-09 20:45:58 +08:00
LouisLam
94a6c1a344 update language files 2021-09-09 20:38:19 +08:00
Louis Lam
ee9e48fe53 Merge pull request #361 from iomataani/italian-translation-update
Italian translation update
2021-09-09 20:26:07 +08:00
LouisLam
a4aee49b03 Merge remote-tracking branch 'origin/master' 2021-09-09 20:22:44 +08:00
LouisLam
e330875c80 fix restore fail if no isDefault 2021-09-09 20:22:32 +08:00
Paride Barison
44eba70b78 altri errorini sistemati 2021-09-09 14:19:15 +02:00
Paride Barison
c6dab944d1 altra correzione 2021-09-09 14:12:30 +02:00
Ioma Taani
0f4bc5850b Merge branch 'louislam:master' into italian-translation-update 2021-09-09 14:05:00 +02:00
Louis Lam
f37190a73d Update README.md 2021-09-09 18:02:58 +08:00
Louis Lam
29f96fae93 Update README.md 2021-09-09 18:01:15 +08:00
Louis Lam
331ae5ec20 Merge pull request #306 from Ponkhy/import-export
Added import and export function
2021-09-09 16:15:48 +08:00
LouisLam
8ee34c7904 also handle the first beat problem in uptime 2021-09-09 15:55:34 +08:00
LouisLam
4f07c2ea9a uptime calculation fully sum in sql 2021-09-09 15:46:28 +08:00
LouisLam
0b1d0d22ff add indexes, improve queries 2021-09-09 12:43:18 +08:00
LouisLam
24facc79d7 sendHeartbeatList use raw data 2021-09-09 11:57:23 +08:00
LouisLam
9f9c1007d7 increase sqlite cache size from 2MB to 12MB 2021-09-09 11:56:42 +08:00
LouisLam
70a6f0313c update .dockerignore 2021-09-09 11:41:43 +08:00
LouisLam
1d05df6ec9 [docker healthcheck] use retries instead, longer start-period causes starting problem in Docker Swarm mode 2021-09-09 11:41:28 +08:00
Ioma Taani
b54bbc302d migliorata la traduzione 2021-09-08 21:34:48 +02:00
LouisLam
dd283423ab improve smtp with cc, bbc and ignore tls 2021-09-09 01:13:09 +08:00
LouisLam
6006038689 fix monitor.stop() in some cases 2021-09-08 20:00:16 +08:00
LouisLam
a7b50c3630 Revert "add user-monitor-list"
This reverts commit 230a9bfaf9.
2021-09-08 19:54:38 +08:00
LouisLam
0ddbac5109 Revert "move userMonitorList out of server.js"
This reverts commit a7cf14c663.
2021-09-08 19:54:38 +08:00
LouisLam
0f440596c8 Revert "cache last heartbeat list in memory"
This reverts commit 87678ea92d.
2021-09-08 19:54:37 +08:00
LouisLam
5a0fcebd6e update link style 2021-09-08 18:59:30 +08:00
LouisLam
87678ea92d cache last heartbeat list in memory 2021-09-08 18:58:02 +08:00
LouisLam
0bf0cfa104 Merge remote-tracking branch 'origin/master' 2021-09-08 18:56:43 +08:00
LouisLam
a7cf14c663 move userMonitorList out of server.js 2021-09-08 16:27:37 +08:00
Louis Lam
325ce72a71 Merge pull request #352 from iomataani/italian-language
Italian language
2021-09-08 16:12:44 +08:00
Louis Lam
9d00bd9029 Merge pull request #357 from Saibamen/nav_from_dashHome
Route to monitor on `DashboardHome`
2021-09-08 16:11:39 +08:00
Adam Stachowicz
f6a168282e Route to monitor on DashboardHome 2021-09-08 09:55:08 +02:00
Ponkhy
acf6f1883b Fixed missing spaces in backup file 2021-09-08 09:02:24 +02:00
Paride Barison
f44f951e40 Tradotte le ultime cose come richiesto 2021-09-08 08:41:19 +02:00
Ioma Taani
dea9b05d49 Merge branch 'louislam:master' into italian-language 2021-09-08 08:30:10 +02:00
LouisLam
230a9bfaf9 add user-monitor-list 2021-09-08 14:16:30 +08:00
Louis Lam
d761d54d0e Merge pull request #340 from Ponkhy/default-notification
Added the option for default notifications
2021-09-08 12:49:14 +08:00
LouisLam
e37621853e Merge branch 'estonian'
# Conflicts:
#	src/pages/Settings.vue
2021-09-08 11:00:19 +08:00
LouisLam
f2cec60481 minor 2021-09-08 10:48:03 +08:00
LouisLam
2212cf9c08 fix confirmDisableAuth no msg if not tranlated 2021-09-08 10:28:40 +08:00
LouisLam
f7562e00c1 improve notification component, no double template 2021-09-08 10:25:03 +08:00
Ponkhy
678e248966 Added indicator while changing database records 2021-09-08 01:10:19 +02:00
Ponkhy
c1aebef005 Fixed SQL error due notification setup 2021-09-08 01:03:24 +02:00
Ponkhy
1ef4562905 Paused monitors stay paused after import 2021-09-07 23:32:25 +02:00
Ponkhy
69a7c4dab4 Improved importBackup handling 2021-09-07 23:14:44 +02:00
jtagcat
2c3880a326 Added Estonian.
- "Avg.": using ≈, as it's commonly enough known.
   There is no known shorthand for 'average' that I know of.

- Leaved some strings empty, as repeating the same words is useless, just noise.
2021-09-07 23:31:48 +03:00
Ponkhy
f4e885982d Added export/import description for de-DE 2021-09-07 21:10:47 +02:00
Ioma Taani
dcf6dd8b32 Ultimi aggiustamenti 2021-09-07 19:16:29 +02:00
Ioma Taani
093cfec8dd Typo. 2021-09-07 19:13:04 +02:00
Ioma Taani
f13dcb3c5b Update it-IT.js
Completed.
2021-09-07 19:07:50 +02:00
LouisLam
1a5ffd4755 add description for export/import 2021-09-08 00:46:48 +08:00
Ioma Taani
a2f9e9e9c4 Creata lingua italiana 2021-09-07 18:25:24 +02:00
LouisLam
62712f5cc4 Merge branch 'master' into import-export 2021-09-08 00:18:43 +08:00
LouisLam
f0f1847ad8 update language files 2021-09-07 23:53:20 +08:00
LouisLam
0aeaf87f5b Merge branch 'master' into default-notification
# Conflicts:
#	server/notification.js
#	src/components/NotificationDialog.vue
2021-09-07 23:40:42 +08:00
LouisLam
5ca0dd628d remove debug msg 2021-09-07 23:36:55 +08:00
LouisLam
4a106431f3 convert Telegram into a vue components 2021-09-07 23:32:05 +08:00
LouisLam
d164b6ccce prevent Chrome ask for saving password for notification settings (change to one-time-code to solve it) 2021-09-07 23:06:49 +08:00
LouisLam
da74391c3e convert notifications into modules 2021-09-07 22:42:46 +08:00
Louis Lam
850563442a Merge pull request #349 from Ponkhy/redirect-after-setup
Auto login after setup
2021-09-07 21:16:54 +08:00
Ponkhy
242e494cb5 Merge branch 'master' into import-export 2021-09-07 14:12:53 +02:00
Ponkhy
4faa409027 Merge branch 'master' into default-notification 2021-09-07 14:07:50 +02:00
Louis Lam
f842fb409e Update README.md 2021-09-07 17:44:51 +08:00
Louis Lam
72dd894856 Update README.md 2021-09-07 17:44:39 +08:00
LouisLam
0b0417e064 Merge remote-tracking branch 'origin/master' 2021-09-07 17:40:36 +08:00
LouisLam
fe35d95441 merge language files 2021-09-07 17:38:04 +08:00
LouisLam
da131a5156 Merge branch 'master' into clear-monitor-data
# Conflicts:
#	src/languages/da-DK.js
#	src/languages/en.js
#	src/languages/es-ES.js
#	src/languages/fr-FR.js
#	src/languages/ja.js
#	src/languages/ko-KR.js
#	src/languages/nl-NL.js
#	src/languages/ru-RU.js
#	src/languages/sr-latn.js
#	src/languages/sr.js
#	src/languages/sv-SE.js
#	src/languages/zh-CN.js
#	src/languages/zh-HK.js
2021-09-07 17:36:37 +08:00
Louis Lam
926c15ea40 Update healthcheck.js 2021-09-07 17:30:50 +08:00
LouisLam
87e874406a Merge branch 'Ponkhy_master_hidden' 2021-09-07 17:09:24 +08:00
LouisLam
0e288ea92d disable chrome save password dialog for autocompelete = off only 2021-09-07 17:06:33 +08:00
LouisLam
8a4a87716f disable chrome save password dialog 2021-09-07 16:46:42 +08:00
Ponkhy
d01fa9bcfc Auto login after setup 2021-09-07 10:25:25 +02:00
LouisLam
ec8c1cad31 update healthcheck.js for changing ssl cert / hostname / port 2021-09-07 16:06:28 +08:00
LouisLam
fb0fa2a843 add sqlite cli to docker 2021-09-07 15:36:29 +08:00
LouisLam
f5c6d422d5 switch debian to be the main docker base 2021-09-07 15:31:32 +08:00
Louis Lam
ea4240455f Merge pull request #347 from gaby/refactor-docker
Debian Dockerfile refactoring and improvements
2021-09-07 15:21:19 +08:00
Juan Calderon-Perez
ce30ee74e9 Revert changes breaking ARM releases 2021-09-07 01:26:42 -04:00
Juan Calderon-Perez
66b5f157eb Update Dockerfile order to improve layer caching. 2021-09-07 00:13:07 -04:00
Juan Calderon-Perez
5fe8a0917a Refactor Debian Dockerfile 2021-09-06 22:20:12 -04:00
Ponkhy
848296b77a Changed input for sensitive data to HiddenInput 2021-09-07 02:30:53 +02:00
Ponkhy
edfaacbb5f Added component HiddenInput.vue 2021-09-07 02:24:02 +02:00
Ponkhy
bb8385d690 Merge branch 'default-notification' of https://github.com/Ponkhy/uptime-kuma into default-notification 2021-09-06 09:46:36 +02:00
Ponkhy
b95404b6e0 Fixed suggestions 2021-09-06 09:45:52 +02:00
Louis Lam
cc351b2883 Merge pull request #339 from Minvinea/master
Update few words and file name
2021-09-06 13:38:26 +08:00
Louis Lam
3c06f249ca Merge pull request #338 from Saibamen/pl_fixes
[LANG] `pl` fixes
2021-09-06 12:44:56 +08:00
Minvinea
31648dc5e8 Update #2 2021-09-05 23:41:57 +02:00
Minvinea
1197cfa11e Update file name 2021-09-05 23:37:26 +02:00
Ponkhy
fd8c95d64e Merge branch 'master' into default-notification 2021-09-05 23:32:31 +02:00
Ponkhy
58240aceef Added the option for default notifications 2021-09-05 23:23:06 +02:00
Adam Stachowicz
e8b814733d Fix typo 2021-09-05 23:14:56 +02:00
Minvinea
dcc7799108 Update few words and name of file
https://prnt.sc/1rbosiy
2021-09-05 21:34:32 +02:00
Adam Stachowicz
424f20f8e1 Better upsideDownModeDescription 2021-09-05 20:59:04 +02:00
Adam Stachowicz
d4ff5d8b32 [LANG] pl fixes 2021-09-05 20:55:25 +02:00
LouisLam
899b33b3a9 add language missing keys 2021-09-06 01:31:05 +08:00
Ponkhy
1b8b33c4c3 Added set language to the Setup.vue 2021-09-05 15:40:35 +02:00
Louis Lam
d34ed00841 Merge pull request #333 from Misly16/master
Add Polish Locale
2021-09-05 12:03:07 +08:00
Louis Lam
f9c177b150 Merge pull request #315 from tgxn/patch-1
Notifications Content Improvements: SMTP, LunaSea, PushBullet
2021-09-05 11:52:50 +08:00
Misly
9952463350 Add Polish Locale 2021-09-04 20:38:53 +00:00
LouisLam
5837c353b7 change the default theme to auto from light 2021-09-05 02:47:31 +08:00
LouisLam
d5b32ffbb8 add language missing keys 2021-09-05 02:25:40 +08:00
LouisLam
61936ae5eb Merge remote-tracking branch 'Ponkhy/clear-monitor-data' into clear-monitor-data 2021-09-05 02:04:48 +08:00
LouisLam
299506ce45 reset the heartbeat list instead of reload the page after cleared events or heartbeats 2021-09-05 02:03:40 +08:00
LouisLam
ca8d4ab61b Merge branch 'master' into clear-monitor-data 2021-09-05 00:24:09 +08:00
LouisLam
ffbdf97478 improve heartbeat bar rendering in different dpi 2021-09-05 00:08:24 +08:00
LouisLam
cc25787878 improve heartbeat bar rendering in different dpi 2021-09-04 23:32:46 +08:00
Louis Lam
17453a8812 Merge pull request #332 from LeviSnoot/master
Update & Improve Swedish Translation
2021-09-04 23:16:01 +08:00
Levi
f1a151b4a1 Update & Improve Swedish Translation 2021-09-04 16:03:34 +02:00
Levi
d35b205fcc Update & Improve Swedish Translation 2021-09-04 16:01:41 +02:00
Levi
2a34e41d8c Improved Swedish translation 2021-09-04 15:59:49 +02:00
Domenic Horner
41d32bb9dd Undo parm changes 2021-09-04 20:08:18 +08:00
Ponkhy
0b9e410ea5 Added i18n key to en.js 2021-09-04 13:36:08 +02:00
Ponkhy
904ed326ca Merge branch 'louislam:master' into clear-monitor-data 2021-09-04 13:34:29 +02:00
LouisLam
778995a4fb add dockerfile-debian 2021-09-04 15:48:25 +08:00
LouisLam
ee60d74910 fix dockerfile path 2021-09-04 15:32:55 +08:00
Domenic Horner
b0acda52f9 Add time to smtp body content 2021-09-04 11:27:18 +08:00
Domenic Horner
e9cd9be03a Use constants for UP/DOWN through notifications class 2021-09-04 11:09:34 +08:00
Domenic Horner
6ae279c7f3 Move title generation to notification class 2021-09-04 11:06:06 +08:00
Domenic Horner
9c32adfb55 Update pushbullet down body
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:44 +08:00
Domenic Horner
d346afd33b Update pushbullet up body
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:38 +08:00
Domenic Horner
3bf380c684 Update lunasea "up" body content
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:10 +08:00
Domenic Horner
dca5c59982 Update lunasea body content
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:24:43 +08:00
Ponkhy
8f9a973ede Merge branch 'master' into import-export 2021-09-04 01:26:48 +02:00
Ponkhy
b98ec0c3d4 Added i18n keys if necessary for some languages 2021-09-04 01:26:06 +02:00
LouisLam
ebf362754c Merge branch 'master' into clear-monitor-data 2021-09-03 14:56:43 +08:00
Ponkhy
7d70c4d8cd Code optimizations 2021-09-02 16:13:31 +02:00
Domenic Horner
532ad3044c Add space to pushbullet and lunasea notifications
Start changes regarding standardization of notification messages
2021-09-02 21:55:25 +08:00
Ponkhy
2dd392e609 Added uptime kuma version to backup file 2021-09-02 10:15:25 +02:00
Ponkhy
c13cc62d3d Update server/server.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-01 21:32:33 +02:00
LouisLam
8e1e4b9204 override file-selector-button hover color 2021-09-02 01:51:51 +08:00
LouisLam
66f9ad5754 Merge branch 'master' into import-export 2021-09-02 01:35:42 +08:00
Ponkhy
b604910bbb Merge branch 'master' into clear-monitor-data 2021-09-01 17:17:40 +02:00
Ponkhy
2f6c5963c5 Added import and export function 2021-09-01 17:09:32 +02:00
Ponkhy
7b92166d18 Added clear all db statistics function 2021-09-01 00:36:24 +02:00
Ponkhy
1341d220ed Merge branch 'louislam:master' into clear-monitor-data 2021-08-31 23:22:45 +02:00
Ponkhy
1952e34110 Added the possibility to clear monitor data 2021-08-29 18:47:01 +02:00
Nelson Chan
b9a6088f50 WIP: Add login page, nav bar buttons 2021-07-30 15:27:18 +08:00
123 changed files with 11086 additions and 4489 deletions

View File

@@ -2,8 +2,12 @@
/dist
/node_modules
/data
/out
/test
/kubernetes
/.do
**/.dockerignore
/private
**/.git
**/.gitignore
**/docker-compose*
@@ -20,6 +24,11 @@ yarn.lock
app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
CNAME
install.sh
SECURITY.md
tsconfig.json
### .gitignore content (commented rules are duplicated)

View File

@@ -1,4 +1,5 @@
module.exports = {
root: true,
env: {
browser: true,
commonjs: true,
@@ -16,8 +17,10 @@ module.exports = {
requireConfigFile: false,
},
rules: {
"linebreak-style": ["error", "unix"],
"camelcase": ["warn", {
"properties": "never"
"properties": "never",
"ignoreImports": true
}],
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
@@ -31,11 +34,12 @@ module.exports = {
},
],
quotes: ["warn", "double"],
//semi: ['off', 'never'],
semi: "warn",
"vue/html-indent": ["warn", 4], // default: 2
"vue/max-attributes-per-line": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/html-self-closing": "off",
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
"no-multi-spaces": ["error", {
ignoreEOLComments: true,
}],
@@ -81,4 +85,12 @@ module.exports = {
"one-var": ["error", "never"],
"max-statements-per-line": ["error", { "max": 1 }]
},
}
"overrides": [
{
"files": [ "src/languages/*.js", "src/icon.js" ],
"rules": {
"comma-dangle": ["error", "always-multiline"],
}
}
]
};

View File

@@ -12,6 +12,7 @@ Please search in Issues without filters: https://github.com/louislam/uptime-kuma
**Info**
Uptime Kuma Version:
Using Docker?: Yes/No
Docker Version:
Node.js Version (Without Docker only):
OS:
Browser:

View File

@@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -23,12 +24,13 @@ 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:
Uptime Kuma Version:
Using Docker?: Yes/No
Docker Version:
Node.js Version (Without Docker only):
OS:
Browser:
**Screenshots**
If applicable, add screenshots to help explain your problem.
@@ -36,3 +38,5 @@ If applicable, add screenshots to help explain your problem.
**Error Log**
It is easier for us to find out the problem.
Docker: `docker logs <container id>`
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)

5
.gitignore vendored
View File

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

View File

@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within

View File

@@ -2,7 +2,7 @@
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
@@ -20,13 +20,13 @@ If you are not sure, feel free to create an empty pull request draft first.
- Add a chart
- Fix a bug
### *️⃣ Requires one more reviewer
### *️⃣ Requires one more reviewer
I do not have such knowledge to test it.
- Add k8s supports
- Add k8s supports
### *️⃣ Low Priority
### *️⃣ Low Priority
It changed my current workflow and require further studies.
@@ -41,9 +41,9 @@ It changed my current workflow and require further studies.
# Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
@@ -52,8 +52,8 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
# Coding Styles
- Follow .editorconfig
- Follow eslint
- Follow `.editorconfig`
- Follow ESLint
## Name convention
@@ -62,12 +62,13 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- CSS/SCSS: dash-type
# Tools
- Node.js >= 14
- Git
- IDE that supports .editorconfig and eslint (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)
# Install dependencies
# Install dependencies
```bash
npm install --dev
@@ -75,32 +76,29 @@ npm install --dev
For npm@7, you need --legacy-peer-deps
```
```bash
npm install --legacy-peer-deps --dev
```
# Backend Dev
(2021-09-23 Update)
```bash
npm run start-server
# Or
node server/server.js
npm run start-server-dev
```
It binds to 0.0.0.0:3001 by default.
It binds to `0.0.0.0:3001` by default.
## Backend Details
It is mainly a socket.io app + express.js.
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
# Frontend Dev
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
Start frontend dev server. Hot-reload enabled in this way. It binds to `0.0.0.0:3000` by default.
```bash
npm run dev
@@ -108,7 +106,7 @@ npm run dev
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
You can use Vue Devtool Chrome extension for debugging.
You can use Vue.js devtools Chrome extension for debugging.
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
@@ -118,8 +116,7 @@ localStorage.dev = "dev";
So that the frontend will try to connect websocket server in 3001.
Alternately, you can specific NODE_ENV to "development".
Alternately, you can specific `NODE_ENV` to "development".
## Build the frontend
@@ -131,22 +128,17 @@ npm run build
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
The router in "src/main.js"
The router is in `src/router.js`
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
The data and socket logic in "src/mixins/socket.js"
The data and socket logic are in `src/mixins/socket.js`.
# Database Migration
1. create `patch{num}.sql` in `./db/`
1. update `latestVersion` in `./server/database.js`
1. Create `patch{num}.sql` in `./db/`
2. Update `latestVersion` in `./server/database.js`
# Unit Test
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.

View File

@@ -20,13 +20,13 @@ It is a 5 minutes live demo, all data will be deleted after that. The server is
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 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).
* 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.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
## 🔧 How to Install
@@ -44,6 +44,9 @@ Browse to http://localhost:3001 after started.
Required Tools: Node.js >= 14, git and pm2.
```bash
# Update your npm to the latest version
npm install npm -g
git clone https://github.com/louislam/uptime-kuma.git
cd uptime-kuma
npm run setup
@@ -64,7 +67,6 @@ If you need more options or need to browse via a reserve proxy, please read:
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
## 🆙 How to Update
Please read:
@@ -106,9 +108,21 @@ Telegram Notification Sample:
If you love this project, please consider giving me a ⭐.
## 🗣️ Discussion
### Issues Page
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
### Subreddit
My Reddit account: louislamlam
You can mention me if you ask question on Reddit.
https://www.reddit.com/r/UptimeKuma/
## Contribute
If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

View File

@@ -10,5 +10,6 @@ currently being supported with security updates.
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
Please report security issues to uptime@kuma.pet.
https://github.com/louislam/uptime-kuma/issues
Do not use the issue tracker or discuss it in the public as it will cause more damage.

Binary file not shown.

10
db/patch-2fa.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 user
ADD twofa_secret VARCHAR(64);
ALTER TABLE user
ADD twofa_status BOOLEAN default 0 NOT NULL;
COMMIT;

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 retry_interval INTEGER default 0 not null;
COMMIT;

30
db/patch-group-table.sql Normal file
View File

@@ -0,0 +1,30 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
create table `group`
(
id INTEGER not null
constraint group_pk
primary key autoincrement,
name VARCHAR(255) not null,
created_date DATETIME default (DATETIME('now')) not null,
public BOOLEAN default 0 not null,
active BOOLEAN default 1 not null,
weight BOOLEAN NOT NULL DEFAULT 1000
);
CREATE TABLE [monitor_group]
(
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
weight BOOLEAN NOT NULL DEFAULT 1000
);
CREATE INDEX [fk]
ON [monitor_group] (
[monitor_id],
[group_id]);
COMMIT;

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;
-- For sendHeartbeatList
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
-- For sendImportantHeartbeatList
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
COMMIT;

View File

@@ -0,0 +1,18 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
create table incident
(
id INTEGER not null
constraint incident_pk
primary key autoincrement,
title VARCHAR(255) not null,
content TEXT not null,
style VARCHAR(30) default 'warning' not null,
created_date DATETIME default (DATETIME('now')) not null,
last_updated_date DATETIME,
pin BOOLEAN default 1 not null,
active BOOLEAN default 1 not null
);
COMMIT;

View File

@@ -0,0 +1,22 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Generated by Intellij IDEA
create table setting_dg_tmp
(
id INTEGER
primary key autoincrement,
key VARCHAR(200) not null
unique,
value TEXT,
type VARCHAR(20)
);
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting;
drop table setting;
alter table setting_dg_tmp rename to setting;
COMMIT;

19
db/patch10.sql Normal file
View File

@@ -0,0 +1,19 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
CREATE TABLE tag (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
color VARCHAR(255) NOT NULL,
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
);
CREATE TABLE monitor_tag (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
value TEXT,
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);

7
db/patch9.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 notification
ADD is_default BOOLEAN default 0 NOT NULL;
COMMIT;

View File

@@ -1,26 +1,57 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS release
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim AS build
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 git && \
ln -s /usr/bin/python3 /usr/bin/python && \
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
COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune
RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-buster-slim AS release
WORKDIR /app
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
RUN apt update && \
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise && \
rm -rf /var/lib/apt/lists/*
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly
# Upload the artifact to Github
FROM node:14-buster-slim AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file
ARG GITHUB_TOKEN
ARG TARGETARCH
ARG PLATFORM=debian
ARG VERSION=1.5.0
COPY --from=build /app /app
RUN FILE=uptime-kuma.tar.gz
RUN tar -czf $FILE app
RUN curl \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: $(file -b --mime-type $FILE)" \
--data-binary @$FILE \
"https://uploads.github.com/repos/louislam/uptime-kuma/releases/$VERSION/assets?name=$(basename $FILE)"

30
dockerfile-alpine Normal file
View File

@@ -0,0 +1,30 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS build
WORKDIR /app
COPY . .
RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-alpine3.12 AS release
WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

21
extra/entrypoint.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env sh
# set -e Exit the script if an error happens
set -e
PUID=${PUID=0}
PGID=${PGID=0}
files_ownership () {
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
# -R Recursively descends the specified directories
# -c Like verbose but report only when a change is made
chown -hRc "$PUID":"$PGID" /app/data
}
echo "==> Performing startup jobs and maintenance tasks"
files_ownership
echo "==> Starting application with user $PUID group $PGID"
# --clear-groups Clear supplementary groups.
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"

View File

@@ -1,19 +1,34 @@
let http = require("http");
/*
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
let client;
if (process.env.SSL_KEY && process.env.SSL_CERT) {
client = require("https");
} else {
client = require("http");
}
let options = {
host: "localhost",
port: "3001",
timeout: 2000,
host: process.env.HOST || "127.0.0.1",
port: parseInt(process.env.PORT) || 3001,
timeout: 28 * 1000,
};
let request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
let request = client.request(options, (res) => {
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on("error", function (err) {
console.log("ERROR");
console.error("Health Check ERROR");
process.exit(1);
});
request.end();

View File

@@ -6,12 +6,14 @@ const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
(async () => {
Database.init(args);
await Database.connect();
try {

View File

@@ -1,4 +1,4 @@
// Need to use es6 to read language files
// Need to use ES6 to read language files
import fs from "fs";
import path from "path";
@@ -14,6 +14,7 @@ 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) {
@@ -24,8 +25,9 @@ const copyRecursiveSync = function (src, dest) {
fs.copyFileSync(src, dest);
}
};
console.log(process.argv)
const baseLangCode = process.argv[2] || "zh-HK";
console.log("Arguments:", process.argv)
const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode);
fs.rmdirSync("./languages", { recursive: true });
copyRecursiveSync("../../src/languages", "./languages");
@@ -33,46 +35,50 @@ 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);
console.log("Files:", files);
for (const file of files) {
if (file.endsWith(".js")) {
console.log("Processing " + file);
const lang = await import("./languages/" + file);
if (!file.endsWith(".js")) {
console.log("Skipping " + file)
continue;
}
let obj;
console.log("Processing " + file);
const lang = await import("./languages/" + file);
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];
}
let obj;
if (lang.default) {
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];
}
}
if (baseLang !== en) {
// 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);
}
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");
console.log("Done. Fixing formatting by ESLint...");

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title>

View File

@@ -1,28 +1,32 @@
# Uptime-Kuma K8s Deployment
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
## 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
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
## What do I have to edit?
## 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
- 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
- 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:
## How To use
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
- 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```
- 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.
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.

View File

@@ -30,6 +30,9 @@ spec:
command:
- node
- extra/healthcheck.js
initialDelaySeconds: 180
periodSeconds: 60
timeoutSeconds: 30
readinessProbe:
httpGet:
path: /

3930
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.5.3",
"version": "1.7.3",
"license": "MIT",
"repository": {
"type": "git",
@@ -10,20 +10,26 @@
"node": "14.*"
},
"scripts": {
"install-legacy": "npm install --legacy-peer-deps",
"update-legacy": "npm update --legacy-peer-deps",
"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",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"build": "vite build",
"tsc": "tsc",
"vite-preview-dist": "vite preview --host",
"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": "npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.3-alpine --target release . --push",
"build-docker-debian": "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.7.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.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-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --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",
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.7.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",
@@ -32,61 +38,72 @@
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.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"
"update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
},
"dependencies": {
"@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.3",
"@louislam/sqlite3": "^5.0.6",
"@popperjs/core": "^2.10.1",
"args-parser": "^1.3.0",
"axios": "^0.21.1",
"axios": "^0.21.4",
"bcryptjs": "^2.4.3",
"bootstrap": "^5.1.0",
"bootstrap": "^5.1.1",
"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",
"dayjs": "^1.10.7",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.4",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3",
"nodemailer": "^6.6.5",
"notp": "^2.0.3",
"password-hash": "^1.2.2",
"prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0",
"qrcode": "^1.4.4",
"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",
"thirty-two": "^1.0.2",
"timezones-list": "^3.0.1",
"v-pagination-3": "^0.1.6",
"vue": "^3.2.8",
"vue-chart-3": "^0.5.7",
"vue": "next",
"vue-chart-3": "^0.5.8",
"vue-confirm-dialog": "^1.0.2",
"vue-contenteditable": "^3.0.4",
"vue-i18n": "^9.1.7",
"vue-image-crop-upload": "^3.0.3",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-qrcode": "^1.0.0",
"vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1"
"vue-toastification": "^2.0.0-rc.1",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@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",
"@babel/eslint-parser": "^7.15.7",
"@types/bootstrap": "^5.1.6",
"@vitejs/plugin-legacy": "^1.5.3",
"@vitejs/plugin-vue": "^1.9.1",
"@vue/compiler-sfc": "^3.2.16",
"core-js": "^3.18.0",
"cross-env": "^7.0.3",
"dns2": "^2.0.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.17.0",
"sass": "^1.38.2",
"eslint-plugin-vue": "^7.18.0",
"sass": "^1.42.1",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.4.2",
"vite": "^2.5.3"
"typescript": "^4.4.3",
"vite": "2.5.*"
}
}

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

19
public/manifest.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "Uptime Kuma",
"short_name": "Uptime Kuma",
"start_url": "/",
"background_color": "#fff",
"display": "standalone",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -18,7 +18,7 @@ exports.startInterval = () => {
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
res.data.version = "1000.0.0"
res.data.version = "1000.0.0";
}
exports.latestVersion = res.data.version;

88
server/client.js Normal file
View File

@@ -0,0 +1,88 @@
/*
* For Client Socket
*/
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { io } = require("./server");
async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.export())
}
io.to(socket.userID).emit("notificationList", result)
timeLogger.print("Send Notification List");
return list;
}
/**
* Send Heartbeat History list to socket
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
*/
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID,
])
let result = list.reverse();
if (toUser) {
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
} else {
socket.emit("heartbeatList", monitorID, result, overwrite);
}
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
}
/**
* Important Heart beat list (aka event list)
* @param socket
* @param monitorID
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
*/
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT 500
`, [
monitorID,
])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
if (toUser) {
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
} else {
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
}
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
sendHeartbeatList,
}

View File

@@ -1,42 +1,121 @@
const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { debug, sleep } = require("../src/util");
const dayjs = require("dayjs");
const knex = require("knex");
/**
* Database & App Data Folder
*/
class Database {
static templatePath = "./db/kuma.db"
static templatePath = "./db/kuma.db";
/**
* Data Dir (Default: ./data)
*/
static dataDir;
/**
* User Upload Dir (Default: ./data/upload)
*/
static uploadDir;
static path;
static latestVersion = 8;
/**
* @type {boolean}
*/
static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/**
* Add patch filename in key
* Values:
* true: Add it regardless of order
* false: Do nothing
* { parents: []}: Need parents before add it
*/
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
"patch-2fa.sql": true,
"patch-add-retry-interval-monitor.sql": true,
"patch-incident-table.sql": true,
"patch-group-table.sql": true,
}
/**
* The finally version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature
*/
static latestVersion = 10;
static noReject = true;
static sqliteInstance = null;
static init(args) {
// 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 });
}
Database.uploadDir = Database.dataDir + "upload/";
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
console.log(`Data Dir: ${Database.dataDir}`);
}
static async connect() {
const acquireConnectionTimeout = 120 * 1000;
R.setup("sqlite", {
filename: Database.path,
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
const knexInstance = knex({
client: Dialect,
connection: {
filename: Database.path,
acquireConnectionTimeout: acquireConnectionTimeout,
},
useNullAsDefault: true,
acquireConnectionTimeout: acquireConnectionTimeout,
}, {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
pool: {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
}
});
R.setup(knexInstance);
if (process.env.SQL_LOG === "1") {
R.debug(true);
}
// Auto map the model to a bean object
R.freeze(true)
R.freeze(true);
await R.autoloadModels("./server/model");
await R.exec("PRAGMA foreign_keys = ON");
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
await R.exec("PRAGMA cache_size = -12000");
console.log("SQLite config:");
console.log(await R.getAll("PRAGMA journal_mode"));
console.log(await R.getAll("PRAGMA cache_size"));
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
}
static async patch() {
@@ -54,21 +133,9 @@ class Database {
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else {
console.info("Database patch is needed")
console.info("Database patch is needed");
console.info("Backup the db")
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);
}
this.backup(version);
// Try catch anything here, if gone wrong, restore the backup
try {
@@ -79,18 +146,95 @@ class Database {
console.info(`Patched ${sqlFile}`);
await setSetting("database_version", i);
}
console.log("Database Patched Successfully");
} catch (ex) {
await Database.close();
console.error("Patch db failed!!! Restoring the backup")
fs.copyFileSync(backupPath, Database.path);
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed")
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1);
}
}
await this.patch2();
}
/**
* Call it from patch() only
* @returns {Promise<void>}
*/
static async patch2() {
console.log("Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
debug("Patched files:");
debug(databasePatchedFiles);
try {
for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
}
if (this.patched) {
console.log("Database Patched Successfully");
}
} catch (ex) {
await Database.close();
console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1);
}
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Used it patch2() only
* @param sqlFilename
* @param databasePatchedFiles
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
console.log(sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
console.log(sqlFilename + " is not patched");
if (value.parents) {
console.log(sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
}
this.backup(dayjs().format("YYYYMMDDHHmmss"));
console.log(sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
console.log(sqlFilename + " is patched successfully");
} else {
debug(sqlFilename + " is already patched, skip");
}
}
/**
@@ -107,12 +251,12 @@ class Database {
// Remove all comments (--)
let lines = text.split("\n");
lines = lines.filter((line) => {
return ! line.startsWith("--")
return ! line.startsWith("--");
});
// Split statements by semicolon
// Filter out empty line
text = lines.join("\n")
text = lines.join("\n");
let statements = text.split(";")
.map((statement) => {
@@ -120,7 +264,7 @@ class Database {
})
.filter((statement) => {
return statement !== "";
})
});
for (let statement of statements) {
await R.exec(statement);
@@ -136,10 +280,96 @@ class Database {
* @returns {Promise<void>}
*/
static async close() {
if (this.sqliteInstance) {
this.sqliteInstance.close();
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB");
while (true) {
Database.noReject = true;
await R.close();
await sleep(2000);
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db");
}
}
console.log("SQLite closed");
process.removeListener("unhandledRejection", listener);
}
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param version
*/
static backup(version) {
if (! this.backupPath) {
console.info("Backup the db");
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
this.backupShmPath = shmPath + ".bak" + version;
fs.copyFileSync(shmPath, this.backupShmPath);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
}
}
/**
*
*/
static restore() {
if (this.backupPath) {
console.error("Patch db failed!!! Restoring the backup");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
// Delete patch failed db
try {
if (fs.existsSync(Database.path)) {
fs.unlinkSync(Database.path);
}
if (fs.existsSync(shmPath)) {
fs.unlinkSync(shmPath);
}
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
}
} catch (e) {
console.log("Restore failed, you may need to restore the backup manually");
process.exit(1);
}
// Restore backup
fs.copyFileSync(this.backupPath, Database.path);
if (this.backupShmPath) {
fs.copyFileSync(this.backupShmPath, shmPath);
}
if (this.backupWalPath) {
fs.copyFileSync(this.backupWalPath, walPath);
}
} else {
console.log("Nothing to restore");
}
console.log("Stopped database");
}
}

57
server/image-data-uri.js Normal file
View File

@@ -0,0 +1,57 @@
/*
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
Modified with 0 dependencies
*/
let fs = require("fs");
let ImageDataURI = (() => {
function decode(dataURI) {
if (!/data:image\//.test(dataURI)) {
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
return null;
}
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)");
return {
imageType: regExMatches[1],
dataBase64: regExMatches[2],
dataBuffer: new Buffer(regExMatches[2], "base64")
};
}
function encode(data, mediaType) {
if (!data || !mediaType) {
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
return null;
}
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType;
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64");
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64;
return dataImgBase64;
}
function outputFile(dataURI, filePath) {
filePath = filePath || "./";
return new Promise((resolve, reject) => {
let imageDecoded = decode(dataURI);
fs.writeFile(filePath, imageDecoded.dataBuffer, err => {
if (err) {
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4));
}
resolve(filePath);
});
});
}
return {
decode: decode,
encode: encode,
outputFile: outputFile,
};
})();
module.exports = ImageDataURI;

34
server/model/group.js Normal file
View File

@@ -0,0 +1,34 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
class Group extends BeanModel {
async toPublicJSON() {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON());
}
return {
id: this.id,
name: this.name,
weight: this.weight,
monitorList,
};
}
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.* FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight
`, [
this.id,
]));
}
}
module.exports = Group;

View File

@@ -1,8 +1,8 @@
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc")
let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc)
dayjs.extend(timezone)
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const { BeanModel } = require("redbean-node/dist/bean-model");
/**
@@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
*/
class Heartbeat extends BeanModel {
toPublicJSON() {
return {
status: this.status,
time: this.time,
msg: "", // Hide for public
ping: this.ping,
};
}
toJSON() {
return {
monitorID: this.monitor_id,

18
server/model/incident.js Normal file
View File

@@ -0,0 +1,18 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Incident extends BeanModel {
toPublicJSON() {
return {
id: this.id,
style: this.style,
title: this.title,
content: this.content,
pin: this.pin,
createdDate: this.createdDate,
lastUpdatedDate: this.lastUpdatedDate,
};
}
}
module.exports = Incident;

View File

@@ -1,16 +1,16 @@
const https = require("https");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc")
let timezone = require("dayjs/plugin/timezone")
dayjs.extend(utc)
dayjs.extend(timezone)
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const axios = require("axios");
const { Prometheus } = require("../prometheus");
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 { Notification } = require("../notification");
const version = require("../../package.json").version;
/**
@@ -20,18 +20,35 @@ const version = require("../../package.json").version;
* 2 = PENDING
*/
class Monitor extends BeanModel {
/**
* Return a object that ready to parse to JSON for public
* Only show necessary data to public
*/
async toPublicJSON() {
return {
id: this.id,
name: this.name,
};
}
/**
* Return a object that ready to parse to JSON
*/
async toJSON() {
let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id,
])
]);
for (let bean of list) {
notificationIDList[bean.notification_id] = true;
}
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
return {
id: this.id,
name: this.name,
@@ -43,6 +60,7 @@ class Monitor extends BeanModel {
active: this.active,
type: this.type,
interval: this.interval,
retryInterval: this.retryInterval,
keyword: this.keyword,
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
@@ -52,6 +70,7 @@ class Monitor extends BeanModel {
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
notificationIDList,
tags: tags,
};
}
@@ -60,7 +79,7 @@ class Monitor extends BeanModel {
* @returns {boolean}
*/
getIgnoreTls() {
return Boolean(this.ignoreTls)
return Boolean(this.ignoreTls);
}
/**
@@ -90,12 +109,12 @@ class Monitor extends BeanModel {
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id,
])
]);
}
const isFirstBeat = !previousBeat;
let bean = R.dispense("heartbeat")
let bean = R.dispense("heartbeat");
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.status = DOWN;
@@ -131,7 +150,7 @@ class Monitor extends BeanModel {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
});
bean.msg = `${res.status} - ${res.statusText}`
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used
@@ -141,12 +160,12 @@ class Monitor extends BeanModel {
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) {
if (e.message !== "No TLS certificate in response") {
console.error(e.message)
console.error(e.message);
}
}
}
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
if (this.type === "http") {
bean.status = UP;
@@ -156,26 +175,26 @@ class Monitor extends BeanModel {
// Convert to string for object/array
if (typeof data !== "string") {
data = JSON.stringify(data)
data = JSON.stringify(data);
}
if (data.includes(this.keyword)) {
bean.msg += ", keyword is found"
bean.msg += ", keyword is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but keyword is not found")
throw new Error(bean.msg + ", but keyword is not found");
}
}
} else if (this.type === "port") {
bean.ping = await tcping(this.hostname, this.port);
bean.msg = ""
bean.msg = "";
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname);
bean.msg = ""
bean.msg = "";
bean.status = UP;
} else if (this.type === "dns") {
let startTime = dayjs().valueOf();
@@ -195,7 +214,7 @@ class Monitor extends BeanModel {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
dnsMessage = dnsMessage.slice(0, -2);
} else if (this.dns_resolve_type == "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
@@ -205,7 +224,7 @@ class Monitor extends BeanModel {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
dnsMessage = dnsMessage.slice(0, -2);
}
if (this.dnsLastResult !== dnsMessage) {
@@ -268,20 +287,20 @@ class Monitor extends BeanModel {
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
this.id,
])
]);
let text;
if (bean.status === UP) {
text = "✅ Up"
text = "✅ Up";
} else {
text = "🔴 Down"
text = "🔴 Down";
}
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
@@ -293,30 +312,39 @@ class Monitor extends BeanModel {
bean.important = false;
}
let beatInterval = this.interval;
if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === PENDING) {
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`)
if (this.retryInterval > 0) {
beatInterval = this.retryInterval;
}
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
}
io.to(this.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, this.id, this.user_id)
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);
}
if (! this.isStop) {
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
}
};
beat();
}
stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
}
/**
@@ -402,64 +430,74 @@ class Monitor extends BeanModel {
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
static async calcUptime(duration, monitorID) {
const timeLogger = new TimeLogger();
let sec = duration * 3600;
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
let heartbeatList = await R.getAll(`
SELECT duration, time, status
// Handle if heartbeat duration longer than the target duration
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
let result = await R.getRow(`
SELECT
-- SUM all duration, also trim off the beat out of time window
SUM(
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
) AS total_duration,
-- SUM all uptime duration, also trim off the beat out of time window
SUM(
CASE
WHEN (status = 1)
THEN
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
END
) AS uptime_duration
FROM heartbeat
WHERE time > DATETIME('now', ? || ' hours')
AND monitor_id = ? `, [
-duration,
WHERE time > ?
AND monitor_id = ?
`, [
startTime, startTime, startTime, startTime, startTime,
monitorID,
]);
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
let downtime = 0;
let total = 0;
let uptime;
let totalDuration = result.total_duration;
let uptimeDuration = result.uptime_duration;
let uptime = 0;
// Special handle for the first heartbeat only
if (heartbeatList.length === 1) {
if (heartbeatList[0].status === 1) {
uptime = 1;
} else {
if (totalDuration > 0) {
uptime = uptimeDuration / totalDuration;
if (uptime < 0) {
uptime = 0;
}
} else {
for (let row of heartbeatList) {
let value = parseInt(row.duration)
let time = row.time
// Handle new monitor with only one beat, because the beat's duration = 0
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), "second");
value = sec - trim;
if (value < 0) {
value = 0;
}
}
total += value;
if (row.status === 0 || row.status === 2) {
downtime += value;
}
}
uptime = (total - downtime) / total;
if (uptime < 0) {
uptime = 0;
if (status === UP) {
uptime = 1;
}
}
return uptime;
}
/**
* Send Uptime
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
const uptime = await this.calcUptime(duration, monitorID);
io.to(userID).emit("uptime", monitorID, duration, uptime);
}
}

13
server/model/tag.js Normal file
View File

@@ -0,0 +1,13 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Tag extends BeanModel {
toJSON() {
return {
id: this._id,
name: this._name,
color: this._color,
};
}
}
module.exports = Tag;

View File

@@ -0,0 +1,749 @@
let url = require("url");
let MemoryCache = require("./memory-cache");
let t = {
ms: 1,
second: 1000,
minute: 60000,
hour: 3600000,
day: 3600000 * 24,
week: 3600000 * 24 * 7,
month: 3600000 * 24 * 30,
};
let instances = [];
let matches = function (a) {
return function (b) {
return a === b;
};
};
let doesntMatch = function (a) {
return function (b) {
return !matches(a)(b);
};
};
let logDuration = function (d, prefix) {
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
};
function getSafeHeaders(res) {
return res.getHeaders ? res.getHeaders() : res._headers;
}
function ApiCache() {
let memCache = new MemoryCache();
let globalOptions = {
debug: false,
defaultDuration: 3600000,
enabled: true,
appendKey: [],
jsonp: false,
redisClient: false,
headerBlacklist: [],
statusCodes: {
include: [],
exclude: [],
},
events: {
expire: undefined,
},
headers: {
// 'cache-control': 'no-cache' // example of header overwrite
},
trackPerformance: false,
respectCacheControl: false,
};
let middlewareOptions = [];
let instance = this;
let index = null;
let timers = {};
let performanceArray = []; // for tracking cache hit rate
instances.push(this);
this.id = instances.length;
function debug(a, b, c, d) {
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
return arg !== undefined;
});
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
}
function shouldCacheResponse(request, response, toggle) {
let opt = globalOptions;
let codes = opt.statusCodes;
if (!response) {
return false;
}
if (toggle && !toggle(request, response)) {
return false;
}
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
return false;
}
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
return false;
}
return true;
}
function addIndexEntries(key, req) {
let groupName = req.apicacheGroup;
if (groupName) {
debug("group detected \"" + groupName + "\"");
let group = (index.groups[groupName] = index.groups[groupName] || []);
group.unshift(key);
}
index.all.unshift(key);
}
function filterBlacklistedHeaders(headers) {
return Object.keys(headers)
.filter(function (key) {
return globalOptions.headerBlacklist.indexOf(key) === -1;
})
.reduce(function (acc, header) {
acc[header] = headers[header];
return acc;
}, {});
}
function createCacheObject(status, headers, data, encoding) {
return {
status: status,
headers: filterBlacklistedHeaders(headers),
data: data,
encoding: encoding,
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
};
}
function cacheResponse(key, value, duration) {
let redis = globalOptions.redisClient;
let expireCallback = globalOptions.events.expire;
if (redis && redis.connected) {
try {
redis.hset(key, "response", JSON.stringify(value));
redis.hset(key, "duration", duration);
redis.expire(key, duration / 1000, expireCallback || function () {});
} catch (err) {
debug("[apicache] error in redis.hset()");
}
} else {
memCache.add(key, value, duration, expireCallback);
}
// add automatic cache clearing from duration, includes max limit on setTimeout
timers[key] = setTimeout(function () {
instance.clear(key, true);
}, Math.min(duration, 2147483647));
}
function accumulateContent(res, content) {
if (content) {
if (typeof content == "string") {
res._apicache.content = (res._apicache.content || "") + content;
} else if (Buffer.isBuffer(content)) {
let oldContent = res._apicache.content;
if (typeof oldContent === "string") {
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
}
if (!oldContent) {
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
}
res._apicache.content = Buffer.concat(
[oldContent, content],
oldContent.length + content.length
);
} else {
res._apicache.content = content;
}
}
}
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
// monkeypatch res.end to create cache object
res._apicache = {
write: res.write,
writeHead: res.writeHead,
end: res.end,
cacheable: true,
content: undefined,
};
// append header overwrites if applicable
Object.keys(globalOptions.headers).forEach(function (name) {
res.setHeader(name, globalOptions.headers[name]);
});
res.writeHead = function () {
// add cache control headers
if (!globalOptions.headers["cache-control"]) {
if (shouldCacheResponse(req, res, toggle)) {
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
} else {
res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
}
}
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
return res._apicache.writeHead.apply(this, arguments);
};
// patch res.write
res.write = function (content) {
accumulateContent(res, content);
return res._apicache.write.apply(this, arguments);
};
// patch res.end
res.end = function (content, encoding) {
if (shouldCacheResponse(req, res, toggle)) {
accumulateContent(res, content);
if (res._apicache.cacheable && res._apicache.content) {
addIndexEntries(key, req);
let headers = res._apicache.headers || getSafeHeaders(res);
let cacheObject = createCacheObject(
res.statusCode,
headers,
res._apicache.content,
encoding
);
cacheResponse(key, cacheObject, duration);
// display log entry
let elapsed = new Date() - req.apicacheTimer;
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
debug("_apicache.headers: ", res._apicache.headers);
debug("res.getHeaders(): ", getSafeHeaders(res));
debug("cacheObject: ", cacheObject);
}
}
return res._apicache.end.apply(this, arguments);
};
next();
}
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
if (toggle && !toggle(request, response)) {
return next();
}
let headers = getSafeHeaders(response);
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
// only embed apicache headers when not in production environment
if (process.env.NODE_ENV !== "production") {
Object.assign(headers, {
"apicache-store": globalOptions.redisClient ? "redis" : "memory",
"apicache-version": "1.6.2-modified",
});
}
// unstringify buffers
let data = cacheObject.data;
if (data && data.type === "Buffer") {
data =
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
}
// test Etag against If-None-Match for 304
let cachedEtag = cacheObject.headers.etag;
let requestEtag = request.headers["if-none-match"];
if (requestEtag && cachedEtag === requestEtag) {
response.writeHead(304, headers);
return response.end();
}
response.writeHead(cacheObject.status || 200, headers);
return response.end(data, cacheObject.encoding);
}
function syncOptions() {
for (let i in middlewareOptions) {
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
}
}
this.clear = function (target, isAutomatic) {
let group = index.groups[target];
let redis = globalOptions.redisClient;
if (group) {
debug("clearing group \"" + target + "\"");
group.forEach(function (key) {
debug("clearing cached entry for \"" + key + "\"");
clearTimeout(timers[key]);
delete timers[key];
if (!globalOptions.redisClient) {
memCache.delete(key);
} else {
try {
redis.del(key);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + key + "\")");
}
}
index.all = index.all.filter(doesntMatch(key));
});
delete index.groups[target];
} else if (target) {
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
clearTimeout(timers[target]);
delete timers[target];
// clear actual cached entry
if (!redis) {
memCache.delete(target);
} else {
try {
redis.del(target);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + target + "\")");
}
}
// remove from global index
index.all = index.all.filter(doesntMatch(target));
// remove target from each group that it may exist in
Object.keys(index.groups).forEach(function (groupName) {
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
// delete group if now empty
if (!index.groups[groupName].length) {
delete index.groups[groupName];
}
});
} else {
debug("clearing entire index");
if (!redis) {
memCache.clear();
} else {
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
index.all.forEach(function (key) {
clearTimeout(timers[key]);
delete timers[key];
try {
redis.del(key);
} catch (err) {
console.log("[apicache] error in redis.del(\"" + key + "\")");
}
});
}
this.resetIndex();
}
return this.getIndex();
};
function parseDuration(duration, defaultDuration) {
if (typeof duration === "number") {
return duration;
}
if (typeof duration === "string") {
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
if (split.length === 3) {
let len = parseFloat(split[1]);
let unit = split[2].replace(/s$/i, "").toLowerCase();
if (unit === "m") {
unit = "ms";
}
return (len || 1) * (t[unit] || 0);
}
}
return defaultDuration;
}
this.getDuration = function (duration) {
return parseDuration(duration, globalOptions.defaultDuration);
};
/**
* Return cache performance statistics (hit rate). Suitable for putting into a route:
* <code>
* app.get('/api/cache/performance', (req, res) => {
* res.json(apicache.getPerformance())
* })
* </code>
*/
this.getPerformance = function () {
return performanceArray.map(function (p) {
return p.report();
});
};
this.getIndex = function (group) {
if (group) {
return index.groups[group];
} else {
return index;
}
};
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
let duration = instance.getDuration(strDuration);
let opt = {};
middlewareOptions.push({
options: opt,
});
let options = function (localOptions) {
if (localOptions) {
middlewareOptions.find(function (middleware) {
return middleware.options === opt;
}).localOptions = localOptions;
}
syncOptions();
return opt;
};
options(localOptions);
/**
* A Function for non tracking performance
*/
function NOOPCachePerformance() {
this.report = this.hit = this.miss = function () {}; // noop;
}
/**
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
*/
function CachePerformance() {
/**
* Tracks the hit rate for the last 100 requests.
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 1000 requests.
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 10000 requests.
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 100000 requests.
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
/**
* The number of calls that have passed through the middleware since the server started.
*/
this.callCount = 0;
/**
* The total number of hits since the server started
*/
this.hitCount = 0;
/**
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
*/
this.lastCacheHit = null;
/**
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
*/
this.lastCacheMiss = null;
/**
* Return performance statistics
*/
this.report = function () {
return {
lastCacheHit: this.lastCacheHit,
lastCacheMiss: this.lastCacheMiss,
callCount: this.callCount,
hitCount: this.hitCount,
missCount: this.callCount - this.hitCount,
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
hitRateLast100: this.hitRate(this.hitsLast100),
hitRateLast1000: this.hitRate(this.hitsLast1000),
hitRateLast10000: this.hitRate(this.hitsLast10000),
hitRateLast100000: this.hitRate(this.hitsLast100000),
};
};
/**
* Computes a cache hit rate from an array of hits and misses.
* @param {Uint8Array} array An array representing hits and misses.
* @returns a number between 0 and 1, or null if the array has no hits or misses
*/
this.hitRate = function (array) {
let hits = 0;
let misses = 0;
for (let i = 0; i < array.length; i++) {
let n8 = array[i];
for (let j = 0; j < 4; j++) {
switch (n8 & 3) {
case 1:
hits++;
break;
case 2:
misses++;
break;
}
n8 >>= 2;
}
}
let total = hits + misses;
if (total == 0) {
return null;
}
return hits / total;
};
/**
* Record a hit or miss in the given array. It will be recorded at a position determined
* by the current value of the callCount variable.
* @param {Uint8Array} array An array representing hits and misses.
* @param {boolean} hit true for a hit, false for a miss
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
* Each hit or miss is encoded as to bits as follows:
* 00 means no hit or miss has been recorded in these bits
* 01 encodes a hit
* 10 encodes a miss
*/
this.recordHitInArray = function (array, hit) {
let arrayIndex = ~~(this.callCount / 4) % array.length;
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
let clearMask = ~(3 << bitOffset);
let record = (hit ? 1 : 2) << bitOffset;
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
};
/**
* Records the hit or miss in the tracking arrays and increments the call count.
* @param {boolean} hit true records a hit, false records a miss
*/
this.recordHit = function (hit) {
this.recordHitInArray(this.hitsLast100, hit);
this.recordHitInArray(this.hitsLast1000, hit);
this.recordHitInArray(this.hitsLast10000, hit);
this.recordHitInArray(this.hitsLast100000, hit);
if (hit) {
this.hitCount++;
}
this.callCount++;
};
/**
* Records a hit event, setting lastCacheMiss to the given key
* @param {string} key The key that had the cache hit
*/
this.hit = function (key) {
this.recordHit(true);
this.lastCacheHit = key;
};
/**
* Records a miss event, setting lastCacheMiss to the given key
* @param {string} key The key that had the cache miss
*/
this.miss = function (key) {
this.recordHit(false);
this.lastCacheMiss = key;
};
}
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
performanceArray.push(perf);
let cache = function (req, res, next) {
function bypass() {
debug("bypass detected, skipping cache.");
return next();
}
// initial bypass chances
if (!opt.enabled) {
return bypass();
}
if (
req.headers["x-apicache-bypass"] ||
req.headers["x-apicache-force-fetch"] ||
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
) {
return bypass();
}
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
// if (typeof middlewareToggle === 'function') {
// if (!middlewareToggle(req, res)) return bypass()
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
// return bypass()
// }
// embed timer
req.apicacheTimer = new Date();
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
let key = req.originalUrl || req.url;
// Remove querystring from key if jsonp option is enabled
if (opt.jsonp) {
key = url.parse(key).pathname;
}
// add appendKey (either custom function or response path)
if (typeof opt.appendKey === "function") {
key += "$$appendKey=" + opt.appendKey(req, res);
} else if (opt.appendKey.length > 0) {
let appendKey = req;
for (let i = 0; i < opt.appendKey.length; i++) {
appendKey = appendKey[opt.appendKey[i]];
}
key += "$$appendKey=" + appendKey;
}
// attempt cache hit
let redis = opt.redisClient;
let cached = !redis ? memCache.getValue(key) : null;
// send if cache hit from memory-cache
if (cached) {
let elapsed = new Date() - req.apicacheTimer;
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
}
// send if cache hit from redis
if (redis && redis.connected) {
try {
redis.hgetall(key, function (err, obj) {
if (!err && obj && obj.response) {
let elapsed = new Date() - req.apicacheTimer;
debug("sending cached (redis) version of", key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(
req,
res,
JSON.parse(obj.response),
middlewareToggle,
next,
duration
);
} else {
perf.miss(key);
return makeResponseCacheable(
req,
res,
next,
key,
duration,
strDuration,
middlewareToggle
);
}
});
} catch (err) {
// bypass redis on error
perf.miss(key);
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
}
} else {
perf.miss(key);
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
}
};
cache.options = options;
return cache;
};
this.options = function (options) {
if (options) {
Object.assign(globalOptions, options);
syncOptions();
if ("defaultDuration" in options) {
// Convert the default duration to a number in milliseconds (if needed)
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
}
if (globalOptions.trackPerformance) {
debug("WARNING: using trackPerformance flag can cause high memory usage!");
}
return this;
} else {
return globalOptions;
}
};
this.resetIndex = function () {
index = {
all: [],
groups: {},
};
};
this.newInstance = function (config) {
let instance = new ApiCache();
if (config) {
instance.options(config);
}
return instance;
};
this.clone = function () {
return this.newInstance(this.options());
};
// initialize index
this.resetIndex();
}
module.exports = new ApiCache();

View File

@@ -0,0 +1,14 @@
const apicache = require("./apicache");
apicache.options({
headerBlacklist: [
"cache-control"
],
headers: {
// Disable client side cache, only server side cache.
// BUG! Not working for the second request
"cache-control": "no-cache",
},
});
module.exports = apicache;

View File

@@ -0,0 +1,59 @@
function MemoryCache() {
this.cache = {};
this.size = 0;
}
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
let old = this.cache[key];
let instance = this;
let entry = {
value: value,
expire: time + Date.now(),
timeout: setTimeout(function () {
instance.delete(key);
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key);
}, time)
};
this.cache[key] = entry;
this.size = Object.keys(this.cache).length;
return entry;
};
MemoryCache.prototype.delete = function (key) {
let entry = this.cache[key];
if (entry) {
clearTimeout(entry.timeout);
}
delete this.cache[key];
this.size = Object.keys(this.cache).length;
return null;
};
MemoryCache.prototype.get = function (key) {
let entry = this.cache[key];
return entry;
};
MemoryCache.prototype.getValue = function (key) {
let entry = this.get(key);
return entry && entry.value;
};
MemoryCache.prototype.clear = function () {
Object.keys(this.cache).forEach(function (key) {
this.delete(key);
}, this);
return true;
};
module.exports = MemoryCache;

View File

@@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const child_process = require("child_process");
class Apprise extends NotificationProvider {
name = "apprise";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
if (output) {
if (! output.includes("ERROR")) {
return "Sent Successfully";
}
throw new Error(output)
} else {
return "No output from apprise";
}
}
}
module.exports = Apprise;

View File

@@ -0,0 +1,115 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Discord extends NotificationProvider {
name = "discord";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
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"] == DOWN) {
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"],
},
],
}],
}
if (notification.discordPrefixMessage) {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
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",
},
],
}],
}
if (notification.discordPrefixMessage) {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Discord;

View File

@@ -0,0 +1,28 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Gotify extends NotificationProvider {
name = "gotify";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Gotify;

View File

@@ -0,0 +1,60 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Line extends NotificationProvider {
name = "line";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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"] == DOWN) {
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"] == UP) {
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) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Line;

View File

@@ -0,0 +1,48 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class LunaSea extends NotificationProvider {
name = "lunasea";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
return okMsg;
}
if (heartbeatJSON["status"] == DOWN) {
let downdata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
return okMsg;
}
if (heartbeatJSON["status"] == UP) {
let updata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = LunaSea;

View File

@@ -0,0 +1,123 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Mattermost extends NotificationProvider {
name = "mattermost";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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"] == DOWN) {
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"] == UP) {
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) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Mattermost;

View File

@@ -0,0 +1,36 @@
class NotificationProvider {
/**
* Notification Provider Name
* @type string
*/
name = undefined;
/**
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Return Successful Message
* Throw Error with fail msg
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
throw new Error("Have to override Notification.send(...)");
}
throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
}
}
throw new Error(msg)
}
}
module.exports = NotificationProvider;

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Octopush extends NotificationProvider {
name = "octopush";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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 okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Octopush;

View File

@@ -0,0 +1,50 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Pushbullet extends NotificationProvider {
name = "pushbullet";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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"] == DOWN) {
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"] == UP) {
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) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushbullet;

View File

@@ -0,0 +1,49 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Pushover extends NotificationProvider {
name = "pushover";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
let pushoverlink = "https://api.pushover.net/1/messages.json"
try {
if (heartbeatJSON == null) {
let data = {
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushover;

View File

@@ -0,0 +1,30 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Pushy extends NotificationProvider {
name = "pushy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
"message": "Uptime-Kuma"
},
"notification": {
"body": msg,
"badge": 1,
"sound": "ping.aiff"
}
})
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushy;

View File

@@ -0,0 +1,46 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class RocketChat extends NotificationProvider {
name = "rocket.chat";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = RocketChat;

View File

@@ -0,0 +1,27 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Signal extends NotificationProvider {
name = "signal";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let data = {
"message": msg,
"number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Signal;

View File

@@ -0,0 +1,70 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Slack extends NotificationProvider {
name = "slack";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
},
],
}],
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Slack;

View File

@@ -0,0 +1,48 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
class SMTP extends NotificationProvider {
name = "smtp";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
};
// 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);
let bodyTextContent = msg;
if (heartbeatJSON) {
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
}
// send mail with defined transport object
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: msg,
text: bodyTextContent,
tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
},
});
return "Sent Successfully.";
}
}
module.exports = SMTP;

View File

@@ -0,0 +1,124 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Teams extends NotificationProvider {
name = "teams";
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down`;
} else if (status === UP) {
return `✅ Application [${monitorName}] is back online`;
}
return "Notification";
};
_getThemeColor = (status) => {
if (status === DOWN) {
return "ff0000";
}
if (status === UP) {
return "00e804";
}
return "008cff";
};
_notificationPayloadFactory = ({
status,
monitorMessage,
monitorName,
monitorUrl,
}) => {
const notificationMessage = this._statusMessageFactory(
status,
monitorName
);
const facts = [];
if (monitorName) {
facts.push({
name: "Monitor",
value: monitorName,
});
}
if (monitorUrl) {
facts.push({
name: "URL",
value: monitorUrl,
});
}
return {
"@context": "https://schema.org/extensions",
"@type": "MessageCard",
themeColor: this._getThemeColor(status),
summary: notificationMessage,
sections: [
{
activityImage:
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
activityTitle: "**Uptime Kuma**",
},
{
activityTitle: notificationMessage,
},
{
activityTitle: "**Description**",
text: monitorMessage,
facts,
},
],
};
};
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, payload);
};
_handleGeneralNotification = (webhookUrl, msg) => {
const payload = this._notificationPayloadFactory({
monitorMessage: msg
});
return this._sendNotification(webhookUrl, payload);
};
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
await this._handleGeneralNotification(notification.webhookUrl, msg);
return okMsg;
}
let url;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
status: heartbeatJSON.status,
});
await this._sendNotification(notification.webhookUrl, payload);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Teams;

View File

@@ -0,0 +1,27 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Telegram extends NotificationProvider {
name = "telegram";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
},
})
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
}
}
}
module.exports = Telegram;

View File

@@ -0,0 +1,44 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
class Webhook extends NotificationProvider {
name = "webhook";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
};
let finalData;
let config = {};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
}
} else {
finalData = data;
}
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Webhook;

View File

@@ -1,600 +1,77 @@
const axios = require("axios");
const { R } = require("redbean-node");
const FormData = require("form-data");
const nodemailer = require("nodemailer");
const child_process = require("child_process");
const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify");
const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
const Octopush = require("./notification-providers/octopush");
const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy");
const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMTP = require("./notification-providers/smtp");
const Teams = require("./notification-providers/teams");
const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook");
class Notification {
providerList = {};
static init() {
console.log("Prepare Notification Providers");
this.providerList = {};
const list = [
new Apprise(),
new Discord(),
new Teams(),
new Gotify(),
new Line(),
new LunaSea(),
new Mattermost(),
new Octopush(),
new Pushbullet(),
new Pushover(),
new Pushy(),
new RocketChat(),
new Signal(),
new Slack(),
new SMTP(),
new Telegram(),
new Webhook(),
];
for (let item of list) {
if (! item.name) {
throw new Error("Notification provider without name");
}
if (this.providerList[item.name]) {
throw new Error("Duplicate notification provider name");
}
this.providerList[item.name] = item;
}
}
/**
*
* @param notification
* @param msg
* @param monitorJSON
* @param heartbeatJSON
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Successful msg
* Throw Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
if (notification.type === "telegram") {
try {
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
},
})
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
}
} else if (notification.type === "gotify") {
try {
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "webhook") {
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
};
let finalData;
let config = {};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
}
} else {
finalData = data;
}
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "smtp") {
return await Notification.smtp(notification, msg)
} else if (notification.type === "discord") {
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
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) {
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) {
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;
}
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "signal") {
try {
let data = {
"message": msg,
"number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "pushy") {
try {
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
"message": "Uptime-Kuma"
},
"notification": {
"body": msg,
"badge": 1,
"sound": "ping.aiff"
}
})
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "octopush") {
try {
let config = {
headers: {
"api-key": notification.octopushAPIKey,
"api-login": notification.octopushLogin,
"cache-control": "no-cache"
}
};
let data = {
"recipients": [
{
"phone_number": notification.octopushPhoneNumber
}
],
//octopush not supporting non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": notification.octopushSMSType,
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "slack") {
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
},
],
}],
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "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 {
if (heartbeatJSON == null) {
let data = {
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "apprise") {
return Notification.apprise(notification, msg)
} else if (notification.type === "lunasea") {
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
return okMsg;
}
if (heartbeatJSON["status"] == 0) {
let downdata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
return okMsg;
}
if (heartbeatJSON["status"] == 1) {
let updata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
return okMsg;
}
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "pushbullet") {
try {
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
let config = {
headers: {
"Access-Token": notification.pushbulletAccessToken,
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
let testdata = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == 0) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == 1) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "line") {
try {
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == 0) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == 1) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
if (this.providerList[notification.type]) {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
} else {
throw new Error("Notification type is not supported")
throw new Error("Notification type is not supported");
}
}
@@ -617,8 +94,15 @@ class Notification {
bean.name = notification.name;
bean.user_id = userID;
bean.config = JSON.stringify(notification)
bean.config = JSON.stringify(notification);
bean.is_default = notification.isDefault || false;
await R.store(bean)
if (notification.applyExisting) {
await applyNotificationEveryMonitor(bean.id, userID);
}
return bean;
}
static async delete(notificationID, userID) {
@@ -634,52 +118,6 @@ class Notification {
await R.trash(bean)
}
static async smtp(notification, msg) {
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
};
// 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({
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo,
subject: msg,
text: msg,
});
return "Sent Successfully.";
}
static async apprise(notification, msg) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
if (output) {
if (! output.includes("ERROR")) {
return "Sent Successfully";
}
throw new Error(output)
} else {
return ""
}
}
static checkApprise() {
let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise");
@@ -688,18 +126,24 @@ class Notification {
}
function throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
userID
]);
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
for (let i = 0; i < monitors.length; i++) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
monitors[i].id,
notificationID,
])
if (! checkNotification) {
let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID;
await R.store(relation)
}
}
throw new Error(msg)
}
module.exports = {

View File

@@ -0,0 +1,151 @@
let express = require("express");
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
const { R } = require("redbean-node");
const server = require("../server");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
let router = express.Router();
let cache = apicache.middleware;
router.get("/api/entry-page", async (_, response) => {
allowDevAllOrigin(response);
response.json(server.entryPage);
});
// Status Page Config
router.get("/api/status-page/config", async (_request, response) => {
allowDevAllOrigin(response);
let config = await getSettings("statusPage");
if (! config.statusPageTheme) {
config.statusPageTheme = "light";
}
if (! config.statusPagePublished) {
config.statusPagePublished = true;
}
if (! config.title) {
config.title = "Uptime Kuma";
}
response.json(config);
});
// Status Page - Get the current Incident
// Can fetch only if published
router.get("/api/status-page/incident", async (_, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
if (incident) {
incident = incident.toPublicJSON();
}
response.json({
ok: true,
incident,
});
} catch (error) {
send403(response, error.message);
}
});
// Status Page - Monitor List
// Can fetch only if published
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
const publicGroupList = [];
let list = await R.find("group", " public = 1 ORDER BY weight ");
for (let groupBean of list) {
publicGroupList.push(await groupBean.toPublicJSON());
}
response.json(publicGroupList);
} catch (error) {
send403(response, error.message);
}
});
// Status Page Polling Data
// Can fetch only if published
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
allowDevAllOrigin(response);
try {
await checkPublished();
let heartbeatList = {};
let uptimeList = {};
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
`);
for (let monitorID of monitorIDList) {
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 50
`, [
monitorID,
]);
list = R.convertToBeans("heartbeat", list);
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
const type = 24;
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
}
response.json({
heartbeatList,
uptimeList
});
} catch (error) {
send403(response, error.message);
}
});
async function checkPublished() {
if (! await isPublished()) {
throw new Error("The status page is not published");
}
}
/**
* Default is published
* @returns {Promise<boolean>}
*/
async function isPublished() {
const value = await setting("statusPagePublished");
if (value === null) {
return true;
}
return value;
}
function send403(res, msg = "") {
res.status(403).json({
"status": "fail",
"msg": msg,
});
}
module.exports = router;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
const { R } = require("redbean-node");
const { checkLogin, setSettings } = require("../util-server");
const dayjs = require("dayjs");
const { debug } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
const Database = require("../database");
const apicache = require("../modules/apicache");
module.exports.statusPageSocketHandler = (socket) => {
// Post or edit incident
socket.on("postIncident", async (incident, callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 ");
let incidentBean;
if (incident.id) {
incidentBean = await R.findOne("incident", " id = ?", [
incident.id
]);
}
if (incidentBean == null) {
incidentBean = R.dispense("incident");
}
incidentBean.title = incident.title;
incidentBean.content = incident.content;
incidentBean.style = incident.style;
incidentBean.pin = true;
if (incident.id) {
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
} else {
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
}
await R.store(incidentBean);
callback({
ok: true,
incident: incidentBean.toPublicJSON(),
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("unpinIncident", async (callback) => {
try {
checkLogin(socket);
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
// Save Status Page
// imgDataUrl Only Accept PNG!
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
try {
checkLogin(socket);
apicache.clear();
const header = "data:image/png;base64,";
// Check logo format
// If is image data url, convert to png file
// Else assume it is a url, nothing to do
if (imgDataUrl.startsWith("data:")) {
if (! imgDataUrl.startsWith(header)) {
throw new Error("Only allowed PNG logo.");
}
// Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
config.logo = "/upload/logo.png?t=" + Date.now();
} else {
config.icon = imgDataUrl;
}
// Save Config
await setSettings("statusPage", config);
// Save Public Group List
const groupIDList = [];
let groupOrder = 1;
for (let group of publicGroupList) {
let groupBean;
if (group.id) {
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
group.id
]);
} else {
groupBean = R.dispense("group");
}
groupBean.name = group.name;
groupBean.public = true;
groupBean.weight = groupOrder++;
await R.store(groupBean);
await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
groupBean.id
]);
let monitorOrder = 1;
console.log(group.monitorList);
for (let monitor of group.monitorList) {
let relationBean = R.dispense("monitor_group");
relationBean.weight = monitorOrder++;
relationBean.group_id = groupBean.id;
relationBean.monitor_id = monitor.id;
await R.store(relationBean);
}
groupIDList.push(groupBean.id);
group.id = groupBean.id;
}
// Delete groups that not in the list
debug("Delete groups that not in the list");
const slots = groupIDList.map(() => "?").join(",");
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
callback({
ok: true,
publicGroupList,
});
} catch (error) {
console.log(error);
callback({
ok: false,
msg: error.message,
});
}
});
};

View File

@@ -23,7 +23,7 @@ exports.initJWTSecret = async () => {
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
await R.store(jwtSecretBean);
return jwtSecretBean;
}
};
exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => {
@@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) {
resolve(Math.round(data.max));
});
});
}
};
exports.ping = async (hostname) => {
try {
@@ -57,7 +57,7 @@ exports.ping = async (hostname) => {
throw e;
}
}
}
};
exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
@@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) {
if (err) {
reject(err);
} else if (ms === null) {
reject(new Error(stdout))
reject(new Error(stdout));
} else {
resolve(Math.round(ms))
resolve(Math.round(ms));
}
});
});
}
};
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
@@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
}
});
}
})
}
});
};
exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
@@ -108,29 +108,29 @@ exports.setting = async function (key) {
try {
const v = JSON.parse(value);
debug(`Get Setting: ${key}: ${v}`)
debug(`Get Setting: ${key}: ${v}`);
return v;
} catch (e) {
return value;
}
}
};
exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
])
]);
if (!bean) {
bean = R.dispense("setting")
bean = R.dispense("setting");
bean.key = key;
}
bean.value = JSON.stringify(value);
await R.store(bean)
}
await R.store(bean);
};
exports.getSettings = async function (type) {
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
])
]);
let result = {};
@@ -143,7 +143,7 @@ exports.getSettings = async function (type) {
}
return result;
}
};
exports.setSettings = async function (type, data) {
let keyList = Object.keys(data);
@@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) {
if (bean.type === type) {
bean.value = JSON.stringify(data[key]);
promiseList.push(R.store(bean))
promiseList.push(R.store(bean));
}
}
await Promise.all(promiseList);
}
};
// ssl-checker by @dyaa
// param: res - response object from axios
@@ -218,7 +218,7 @@ exports.checkCertificate = function (res) {
issuer,
fingerprint,
};
}
};
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
@@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) {
}
return false;
}
};
exports.getTotalClientInRoom = (io, roomName) => {
@@ -270,4 +270,31 @@ exports.getTotalClientInRoom = (io, roomName) => {
} else {
return 0;
}
}
};
exports.genSecret = () => {
let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length;
for ( let i = 0; i < 64; i++ ) {
secret += chars.charAt(Math.floor(Math.random() * charsLength));
}
return secret;
};
exports.allowDevAllOrigin = (res) => {
if (process.env.NODE_ENV === "development") {
exports.allowAllOrigin(res);
}
};
exports.allowAllOrigin = (res) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
};
exports.checkLogin = (socket) => {
if (! socket.userID) {
throw new Error("You are not logged in.");
}
};

View File

@@ -144,7 +144,9 @@ h2 {
}
.shadow-box {
background-color: $dark-bg;
&:not(.alert) {
background-color: $dark-bg;
}
}
.form-check-input {
@@ -152,7 +154,7 @@ h2 {
}
.form-switch .form-check-input {
background-color: #131a21;
background-color: #232f3b;
}
a,
@@ -255,6 +257,18 @@ h2 {
background-color: $dark-bg;
}
.monitor-list {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
@media (max-width: 550px) {
.table-shadow-box {
tbody {
@@ -268,6 +282,16 @@ h2 {
}
}
}
.alert {
&.bg-info,
&.bg-warning,
&.bg-danger,
&.bg-light {
color: $dark-font-color2;
}
}
}
/*
@@ -288,3 +312,119 @@ h2 {
transform: translateY(50px);
opacity: 0;
}
.slide-fade-right-enter-active {
transition: all 0.2s $easing-in;
}
.slide-fade-right-leave-active {
transition: all 0.2s $easing-in;
}
.slide-fade-right-enter-from,
.slide-fade-right-leave-to {
transform: translateX(50px);
opacity: 0;
}
.monitor-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;
}
}
}
.alert-success {
color: #122f21;
background-color: $primary;
border-color: $primary;
}
.alert-info {
color: #055160;
background-color: #cff4fc;
border-color: #cff4fc;
}
.alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f8d7da;
}
.btn-success {
color: #fff;
background-color: #4caf50;
border-color: #4caf50;
}
[contenteditable=true] {
transition: all $easing-in 0.2s;
background-color: rgba(239, 239, 239, 0.7);
border-radius: 8px;
&:focus {
outline: 0 solid #eee;
background-color: rgba(245, 245, 245, 0.9);
}
&:hover {
background-color: rgba(239, 239, 239, 0.8);
}
.dark & {
background-color: rgba(239, 239, 239, 0.2);
}
/*
&::after {
margin-left: 5px;
content: "🖊️";
font-size: 13px;
color: #eee;
}
*/
}
.action {
transition: all $easing-in 0.2s;
&:hover {
cursor: pointer;
transform: scale(1.2);
}
}
.vue-image-crop-upload .vicp-wrap {
border-radius: 10px !important;
}

View File

@@ -16,3 +16,5 @@ $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);
$dropdown-border-radius: 0.5rem;

View File

@@ -25,21 +25,32 @@ export default {
type: Number,
required: true,
},
heartbeatList: {
type: Array,
default: null,
}
},
data() {
return {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 3, // Odd number only, even = blurry
beatMargin: 4,
move: false,
maxBeat: -1,
}
},
computed: {
/**
* If heartbeatList is null, get it from $root.heartbeatList
*/
beatList() {
return this.$root.heartbeatList[this.monitorId]
if (this.heartbeatList === null) {
return this.$root.heartbeatList[this.monitorId];
} else {
return this.heartbeatList;
}
},
shortBeatList() {
@@ -118,15 +129,31 @@ export default {
window.removeEventListener("resize", this.resize);
},
beforeMount() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
if (this.heartbeatList === null) {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
}
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
if (! Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (! Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);

View File

@@ -0,0 +1,78 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
:type="visibility"
class="form-control"
:placeholder="placeholder"
:maxlength="maxlength"
:autocomplete="autocomplete"
:required="required"
:readonly="readonly"
>
<a v-if="visibility == 'password'" class="btn btn-outline-primary" @click="showInput()">
<font-awesome-icon icon="eye" />
</a>
<a v-if="visibility == 'text'" class="btn btn-outline-primary" @click="hideInput()">
<font-awesome-icon icon="eye-slash" />
</a>
</div>
</template>
<script>
export default {
props: {
modelValue: {
type: String,
default: ""
},
placeholder: {
type: String,
default: ""
},
maxlength: {
type: Number,
default: 255
},
autocomplete: {
type: String,
default: undefined,
},
required: {
type: Boolean
},
readonly: {
type: String,
default: undefined,
},
},
data() {
return {
visibility: "password",
}
},
computed: {
model: {
get() {
return this.modelValue
},
set(value) {
this.$emit("update:modelValue", value)
}
}
},
created() {
},
methods: {
showInput() {
this.visibility = "text";
},
hideInput() {
this.visibility = "password";
},
}
}
</script>

View File

@@ -4,16 +4,23 @@
<form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal" />
<div class="form-floating">
<div v-if="!tokenRequired" class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3">
<div v-if="!tokenRequired" class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div v-if="tokenRequired">
<div class="form-floating mt-3">
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<label for="floatingToken">{{ $t("Token") }}</label>
</div>
</div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check">
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
@@ -42,16 +49,24 @@ export default {
processing: false,
username: "",
password: "",
token: "",
res: null,
tokenRequired: false,
}
},
methods: {
submit() {
this.processing = true;
this.$root.login(this.username, this.password, (res) => {
this.$root.login(this.username, this.password, this.token, (res) => {
this.processing = false;
this.res = res;
console.log(res)
if (res.tokenRequired) {
this.tokenRequired = true;
} else {
this.res = res;
}
})
},
},

View File

@@ -1,44 +1,69 @@
<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 class="shadow-box mb-3">
<div class="list-header">
<div class="placeholder"></div>
<div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon">
<font-awesome-icon icon="search" />
</a>
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" />
</a>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
</div>
</div>
<div class="monitor-list" :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 }}
<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 class="tags">
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</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-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 v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</div>
</router-link>
</router-link>
</div>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
import Tag from "../components/Tag.vue";
export default {
components: {
Uptime,
HeartbeatBar,
Tag,
},
props: {
scrollbar: {
type: Boolean,
},
},
data() {
return {
searchText: "",
}
},
computed: {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
@@ -68,6 +93,17 @@ export default {
return m1.name.localeCompare(m2.name);
})
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText != "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText))
})
}
return result;
},
},
@@ -75,6 +111,9 @@ export default {
monitorURL(id) {
return "/dashboard/" + id;
},
clearSearchText() {
this.searchText = "";
}
},
}
</script>
@@ -87,57 +126,51 @@ export default {
padding-right: 5px !important;
}
.list {
&.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto;
position: sticky;
top: 10px;
}
.list-header {
border-bottom: 1px solid #dee2e6;
border-radius: 10px 10px 0 0;
margin: -10px;
margin-bottom: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
.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 & {
background-color: #161b22;
border-bottom: 0;
}
}
.dark {
.list {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
@media (max-width: 770px) {
.list-header {
margin: -20px;
margin-bottom: 10px;
padding: 5px;
}
}
.search-wrapper {
display: flex;
align-items: center;
}
.search-icon {
padding: 10px;
color: #c0c0c0;
}
.search-input {
max-width: 15em;
}
.monitorItem {
width: 100%;
}
.tags {
padding-left: 62px;
display: flex;
flex-wrap: wrap;
gap: 0;
}
</style>

View File

@@ -11,12 +11,13 @@
</div>
<div class="modal-body">
<div class="mb-3">
<label for="type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="type" v-model="notification.type" class="form-select">
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="notification-type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
<option value="discord">Discord</option>
<option value="teams">Microsoft Teams</option>
<option value="signal">Signal</option>
<option value="gotify">Gotify</option>
<option value="slack">Slack</option>
@@ -33,48 +34,13 @@
</div>
<div class="mb-3">
<label for="name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="name" v-model="notification.name" type="text" class="form-control" required>
<label for="notification-name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
</div>
<template v-if="notification.type === 'telegram'">
<div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
<div class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
</div>
</div>
<Telegram v-if="notification.type === 'telegram'" />
<div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
Auto Get
</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</template>
<!-- TODO: Convert all into vue components, but not an easy task. -->
<template v-if="notification.type === 'webhook'">
<div class="mb-3">
@@ -100,49 +66,7 @@
</div>
</template>
<template v-if="notification.type === 'smtp'">
<div class="mb-3">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="port" class="form-label">Port</label>
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="mb-3">
<div class="form-check">
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="secure">
Secure
</label>
</div>
<div class="form-text">
Generally, true for 465, false for other ports.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="from-email" class="form-label">From Email</label>
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
</div>
<div class="mb-3">
<label for="to-email" class="form-label">To Email</label>
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
</div>
</template>
<SMTP v-if="notification.type === 'smtp'" />
<template v-if="notification.type === 'discord'">
<div class="mb-3">
@@ -157,6 +81,11 @@
<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>
<div class="mb-3">
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
</div>
</template>
<template v-if="notification.type === 'signal'">
@@ -195,7 +124,7 @@
<template v-if="notification.type === 'gotify'">
<div class="mb-3">
<label for="gotify-application-token" class="form-label">Application Token</label>
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">Server URL</label>
@@ -306,13 +235,13 @@
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
</div>
<p style="margin-top: 8px;">
@@ -323,7 +252,7 @@
<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>
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<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>
@@ -354,9 +283,9 @@
<template v-if="notification.type === 'pushover'">
<div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<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>
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-device" class="form-label">Device</label>
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label>
@@ -442,7 +371,7 @@
<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>
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<p style="margin-top: 8px;">
@@ -453,7 +382,7 @@
<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>
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="form-text">
Line Developers Console - <b>Basic Settings</b>
@@ -469,7 +398,31 @@
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>
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
<Teams v-if="notification.type === 'teams'" />
<div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4">
<div class="form-check form-switch">
<input v-model="notification.isDefault" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Default enabled") }}</label>
</div>
<div class="form-text">
{{ $t("enableDefaultNotificationDescription") }}
</div>
<br>
<div class="form-check form-switch">
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Apply on all existing monitors") }}</label>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
@@ -478,6 +431,7 @@
{{ $t("Test") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
@@ -494,16 +448,23 @@
<script lang="ts">
import { Modal } from "bootstrap"
import { ucfirst } from "../util.ts"
import axios from "axios";
import { useToast } from "vue-toastification"
import Confirm from "./Confirm.vue";
const toast = useToast()
import HiddenInput from "./HiddenInput.vue";
import Telegram from "./notifications/Telegram.vue";
import Teams from "./notifications/Teams.vue";
import SMTP from "./notifications/SMTP.vue";
export default {
components: {
Confirm,
HiddenInput,
Telegram,
Teams,
SMTP,
},
props: {},
emits: ["added"],
data() {
return {
model: null,
@@ -512,22 +473,13 @@ export default {
notification: {
name: "",
type: null,
gotifyPriority: 8,
isDefault: false,
// Do not set default value here, please scroll to show()
},
appriseInstalled: false,
}
},
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
@@ -572,11 +524,13 @@ export default {
this.notification = {
name: "",
type: null,
isDefault: false,
}
// Default set to Telegram
this.notification.type = "telegram"
this.notification.gotifyPriority = 8
// Set Default value here
this.notification.type = "telegram";
this.notification.gotifyPriority = 8;
this.notification.smtpSecure = false;
}
this.modal.show()
@@ -589,7 +543,13 @@ export default {
this.processing = false;
if (res.ok) {
this.modal.hide()
this.modal.hide();
// Emit added event, doesn't emit edit.
if (! this.id) {
this.$emit("added", res.id);
}
}
})
},
@@ -613,32 +573,6 @@ export default {
}
})
},
async autoGetTelegramChatID() {
try {
let res = await axios.get(this.telegramGetUpdatesURL)
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1]
if (update.channel_post) {
this.notification.telegramChatID = update.channel_post.chat.id;
} else if (update.message) {
this.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} catch (error) {
toast.error(error.message)
}
},
},
}
</script>

View File

@@ -0,0 +1,144 @@
<template>
<!-- Group List -->
<Draggable
v-model="$root.publicGroupList"
:disabled="!editMode"
item-key="id"
:animation="100"
>
<template #item="group">
<div class="mb-5 ">
<!-- Group Title -->
<h2 class="group-title">
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
</h2>
<div class="shadow-box monitor-list mt-4 position-relative">
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
{{ $t("No Monitors") }}
</div>
<!-- Monitor List -->
<!-- animation is not working, no idea why -->
<Draggable
v-model="group.element.monitorList"
class="monitor-list"
group="same-group"
:disabled="!editMode"
:animation="100"
item-key="id"
>
<template #item="monitor">
<div class="item">
<div class="row">
<div class="col-9 col-md-8 small-padding">
<div class="info">
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
</div>
</div>
</div>
</template>
</Draggable>
</div>
</div>
</template>
</Draggable>
</template>
<script>
import Draggable from "vuedraggable";
import HeartbeatBar from "./HeartbeatBar.vue";
import Uptime from "./Uptime.vue";
export default {
components: {
Draggable,
HeartbeatBar,
Uptime,
},
props: {
editMode: {
type: Boolean,
required: true,
},
},
data() {
return {
};
},
computed: {
showGroupDrag() {
return (this.$root.publicGroupList.length >= 2);
}
},
created() {
},
methods: {
removeGroup(index) {
this.$root.publicGroupList.splice(index, 1);
},
removeMonitor(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
},
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars";
.no-monitor-msg {
position: absolute;
width: 100%;
top: 20px;
left: 0;
}
.monitor-list {
min-height: 46px;
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.drag {
color: #bbb;
cursor: grab;
}
.remove {
color: $danger;
}
.group-title {
span {
display: inline-block;
min-width: 15px;
}
}
.mobile {
.item {
padding: 13px 0 10px 0;
}
}
</style>

73
src/components/Tag.vue Normal file
View File

@@ -0,0 +1,73 @@
<template>
<div class="tag-wrapper rounded d-inline-flex"
:class="{ 'px-3': size == 'normal',
'py-1': size == 'normal',
'm-2': size == 'normal',
'px-2': size == 'sm',
'py-0': size == 'sm',
'm-1': size == 'sm',
}"
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
>
<span class="tag-text">{{ displayText }}</span>
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
<font-awesome-icon icon="times" />
</span>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
},
remove: {
type: Function,
default: null,
},
size: {
type: String,
default: "normal",
}
},
computed: {
displayText() {
if (this.item.value == "") {
return this.item.name;
} else {
return `${this.item.name}: ${this.item.value}`;
}
}
}
}
</script>
<style lang="scss" scoped>
.tag-wrapper {
color: white;
opacity: 0.85;
.dark & {
opacity: 1;
}
}
.tag-text {
padding-bottom: 1px !important;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.btn-remove {
font-size: 0.9em;
line-height: 24px;
opacity: 0.3;
}
.btn-remove:hover {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,405 @@
<template>
<div>
<h4 class="mb-3">{{ $t("Tags") }}</h4>
<div class="mb-3 p-1">
<tag
v-for="item in selectedTags"
:key="item.id"
:item="item"
:remove="deleteTag"
/>
</div>
<div class="p-1">
<button
type="button"
class="btn btn-outline-secondary btn-add"
:disabled="processing"
@click.stop="showAddDialog"
>
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
</button>
</div>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<vue-multiselect
v-model="newDraftTag.select"
class="mb-2"
:options="tagOptions"
:multiple="false"
:searchable="true"
:placeholder="$t('Add New below or Select...')"
track-by="id"
label="name"
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
<span>
{{ option.name }}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
style="height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
</vue-multiselect>
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
<div class="w-50 pe-2">
<input v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}"
:placeholder="$t('Name')"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
{{ $t("Tag with this name already exist.") }}
</div>
</div>
<div class="w-50 ps-2">
<vue-multiselect
v-model="newDraftTag.color"
:options="colorOptions"
:multiple="false"
:searchable="true"
:placeholder="$t('color')"
track-by="color"
label="name"
select-label=""
deselect-label=""
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
</vue-multiselect>
</div>
</div>
<div class="mb-2">
<input v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}"
:placeholder="$t('value (optional)')"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
{{ $t("Tag with this value already exist.") }}
</div>
</div>
<div class="mb-2">
<button
type="button"
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
@click.stop="addDraftTag"
>
{{ $t("Add") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import Tag from "../components/Tag.vue";
import { useToast } from "vue-toastification"
const toast = useToast()
export default {
components: {
Tag,
VueMultiselect,
},
props: {
preSelectedTags: {
type: Array,
default: () => [],
},
},
data() {
return {
modal: null,
existingTags: [],
processing: false,
newTags: [],
deleteTags: [],
newDraftTag: {
name: null,
select: null,
color: null,
value: "",
invalid: true,
nameInvalid: false,
},
};
},
computed: {
tagOptions() {
const tagOptions = this.existingTags;
for (const tag of this.newTags) {
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
tagOptions.push(tag);
}
}
return tagOptions;
},
selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
},
colorOptions() {
return [
{ name: this.$t("Gray"),
color: "#4B5563" },
{ name: this.$t("Red"),
color: "#DC2626" },
{ name: this.$t("Orange"),
color: "#D97706" },
{ name: this.$t("Green"),
color: "#059669" },
{ name: this.$t("Blue"),
color: "#2563EB" },
{ name: this.$t("Indigo"),
color: "#4F46E5" },
{ name: this.$t("Purple"),
color: "#7C3AED" },
{ name: this.$t("Pink"),
color: "#DB2777" },
]
},
validateDraftTag() {
let nameInvalid = false;
let valueInvalid = false;
let invalid = true;
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
// Undo removing a Tag
nameInvalid = false;
valueInvalid = false;
invalid = false;
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
// Try to create new tag with existing name
nameInvalid = true;
invalid = true;
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
) || (
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
)).length > 0) {
// Try to add a tag with existing name and value
valueInvalid = true;
invalid = true;
} else if (this.newDraftTag.select != null) {
// Select an existing tag, no need to validate
invalid = false;
valueInvalid = false;
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
// Missing form inputs
nameInvalid = false;
invalid = true;
} else {
// Looks valid
invalid = false;
nameInvalid = false;
valueInvalid = false;
}
return {
invalid,
nameInvalid,
valueInvalid,
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.getExistingTags();
},
methods: {
showAddDialog() {
this.modal.show();
},
getExistingTags() {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
this.existingTags = res.tags;
} else {
toast.error(res.msg)
}
});
},
deleteTag(item) {
if (item.new) {
// Undo Adding a new Tag
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
} else {
// Remove an Existing Tag
this.deleteTags.push(item);
}
},
textColor(option) {
if (option.color) {
return "white";
} else {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
}
},
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
// Undo removing a tag
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
} else {
// Add an existing Tag
this.newTags.push({
id: this.newDraftTag.select.id,
color: this.newDraftTag.select.color,
name: this.newDraftTag.select.name,
value: this.newDraftTag.value,
new: true,
})
}
} else {
// Add new Tag
this.newTags.push({
color: this.newDraftTag.color.color,
name: this.newDraftTag.name.trim(),
value: this.newDraftTag.value,
new: true,
})
}
this.clearDraftTag();
},
clearDraftTag() {
this.newDraftTag = {
name: null,
select: null,
color: null,
value: "",
invalid: true,
nameInvalid: false,
};
this.modal.hide();
},
addTagAsync(newTag) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addTag", newTag, resolve);
});
},
addMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
});
},
deleteMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
});
},
onEnter() {
if (!this.validateDraftTag.invalid) {
this.addDraftTag();
}
},
async submit(monitorId) {
console.log(`Submitting tag changes for monitor ${monitorId}...`);
this.processing = true;
for (const newTag of this.newTags) {
let tagId;
if (newTag.id == null) {
// Create a New Tag
let newTagResult;
await this.addTagAsync(newTag).then((res) => {
if (!res.ok) {
toast.error(res.msg);
newTagResult = false;
}
newTagResult = res.tag;
});
if (!newTagResult) {
// abort
this.processing = false;
return;
}
tagId = newTagResult.id;
// Assign the new ID to the tags of the same name & color
this.newTags.map(tag => {
if (tag.name == newTag.name && tag.color == newTag.color) {
tag.id = newTagResult.id;
}
})
} else {
tagId = newTag.id;
}
let newMonitorTagResult;
// Assign tag to monitor
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
newMonitorTagResult = false;
}
newMonitorTagResult = true;
});
if (!newMonitorTagResult) {
// abort
this.processing = false;
return;
}
}
for (const deleteTag of this.deleteTags) {
let deleteMonitorTagResult;
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
deleteMonitorTagResult = false;
}
deleteMonitorTagResult = true;
});
if (!deleteMonitorTagResult) {
// abort
this.processing = false;
return;
}
}
this.getExistingTags();
this.newTags = [];
this.deleteTags = [];
this.processing = false;
}
},
};
</script>
<style scoped>
.btn-add {
width: 100%;
}
.modal-body {
padding: 1.5rem;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("Setup 2FA") }}
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
</h5>
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
<vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
<button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
</div>
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
{{ $t("Enable 2FA") }}
</button>
<button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
{{ $t("Disable 2FA") }}
</button>
<div v-if="uri && twoFAStatus == false" class="mt-3">
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
<div class="input-group">
<input v-model="token" type="text" maxlength="6" class="form-control">
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
</div>
<p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
</div>
</div>
</div>
<div v-if="uri && twoFAStatus == false" class="modal-footer">
<button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
{{ $t("confirmEnableTwoFAMsg") }}
</Confirm>
<Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
{{ $t("confirmDisableTwoFAMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap"
import Confirm from "./Confirm.vue";
import VueQrcode from "vue-qrcode"
import { useToast } from "vue-toastification"
const toast = useToast()
export default {
components: {
Confirm,
VueQrcode,
},
props: {},
data() {
return {
processing: false,
uri: null,
tokenValid: false,
twoFAStatus: null,
token: null,
showURI: false,
}
},
mounted() {
this.modal = new Modal(this.$refs.modal)
this.getStatus();
},
methods: {
show() {
this.modal.show()
},
confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show()
},
confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show()
},
prepare2FA() {
this.processing = true;
this.$root.getSocket().emit("prepare2FA", (res) => {
this.processing = false;
if (res.ok) {
this.uri = res.uri;
} else {
toast.error(res.msg);
}
})
},
save2FA() {
this.processing = true;
this.$root.getSocket().emit("save2FA", (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.getStatus();
this.modal.hide();
} else {
toast.error(res.msg);
}
})
},
disable2FA() {
this.processing = true;
this.$root.getSocket().emit("disable2FA", (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.getStatus();
this.modal.hide();
} else {
toast.error(res.msg);
}
})
},
verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
if (res.ok) {
this.tokenValid = res.valid;
} else {
toast.error(res.msg);
}
})
},
getStatus() {
this.$root.getSocket().emit("twoFAStatus", (res) => {
if (res.ok) {
this.twoFAStatus = res.status;
} else {
toast.error(res.msg);
}
})
},
},
}
</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,75 @@
<template>
<div class="mb-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="mb-3">
<label for="secure" class="form-label">Secure</label>
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
<option :value="false">None / STARTTLS (25, 587)</option>
<option :value="true">TLS (465)</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls-error">
Ignore TLS Error
</label>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ $t("Username") }}</label>
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="from-email" class="form-label">From Email</label>
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder="&quot;Uptime Kuma&quot; &lt;example@kuma.pet&gt;">
<div class="form-text">
</div>
</div>
<div class="mb-3">
<label for="to-email" class="form-label">To Email</label>
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
</div>
<div class="mb-3">
<label for="to-cc" class="form-label">CC</label>
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="to-bcc" class="form-label">BCC</label>
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
data() {
return {
name: "smtp",
}
},
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
<input
id="teams-webhookurl"
v-model="$parent.notification.webhookUrl"
type="text"
class="form-control"
required
/>
<div class="form-text">
You can learn how to create a webhook url
<a
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
target="_blank"
>here</a>.
</div>
</div>
</template>
<script>
export default {
data() {
return {
name: "teams",
};
},
};
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<div class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
</div>
</div>
<div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required>
<button v-if="$parent.notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
{{ $t("Auto Get") }}
</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="$parent.notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
import axios from "axios";
import { useToast } from "vue-toastification"
const toast = useToast();
export default {
components: {
HiddenInput,
},
data() {
return {
name: "telegram",
}
},
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.$parent.notification.telegramBotToken) {
token = this.$parent.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
mounted() {
},
methods: {
async autoGetTelegramChatID() {
try {
let res = await axios.get(this.telegramGetUpdatesURL)
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1]
if (update.channel_post) {
this.notification.telegramChatID = update.channel_post.chat.id;
} else if (update.message) {
this.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} catch (error) {
toast.error(error.message)
}
},
}
}
</script>

52
src/i18n.js Normal file
View File

@@ -0,0 +1,52 @@
import { createI18n } from "vue-i18n";
import bgBG from "./languages/bg-BG";
import daDK from "./languages/da-DK";
import deDE from "./languages/de-DE";
import en from "./languages/en";
import esEs from "./languages/es-ES";
import ptBR from "./languages/pt-BR";
import etEE from "./languages/et-EE";
import frFR from "./languages/fr-FR";
import itIT from "./languages/it-IT";
import ja from "./languages/ja";
import koKR from "./languages/ko-KR";
import nlNL from "./languages/nl-NL";
import pl from "./languages/pl";
import ruRU from "./languages/ru-RU";
import sr from "./languages/sr";
import srLatn from "./languages/sr-latn";
import trTR from "./languages/tr-TR";
import svSE from "./languages/sv-SE";
import zhCN from "./languages/zh-CN";
import zhHK from "./languages/zh-HK";
const languageList = {
en,
"zh-HK": zhHK,
"bg-BG": bgBG,
"de-DE": deDE,
"nl-NL": nlNL,
"es-ES": esEs,
"pt-BR": ptBR,
"fr-FR": frFR,
"it-IT": itIT,
"ja": ja,
"da-DK": daDK,
"sr": sr,
"sr-latn": srLatn,
"sv-SE": svSE,
"tr-TR": trTR,
"ko-KR": koKR,
"ru-RU": ruRU,
"zh-CN": zhCN,
"pl": pl,
"et-EE": etEE,
};
export const i18n = createI18n({
locale: localStorage.locale || "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: true,
messages: languageList,
});

View File

@@ -1,10 +1,60 @@
import { library } from "@fortawesome/fontawesome-svg-core"
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"
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
// Add Free Font Awesome Icons here
// Add Free Font Awesome Icons
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp);
import {
faArrowAltCircleUp,
faCog,
faEdit,
faEye,
faEyeSlash,
faList,
faPause,
faPlay,
faPlus,
faSearch,
faTachometerAlt,
faTimes,
faTimesCircle,
faTrash,
faCheckCircle,
faStream,
faSave,
faExclamationCircle,
faBullhorn,
faArrowsAltV,
faUnlink,
faQuestionCircle,
faImages, faUpload,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faArrowAltCircleUp,
faCog,
faEdit,
faEye,
faEyeSlash,
faList,
faPause,
faPlay,
faPlus,
faSearch,
faTachometerAlt,
faTimes,
faTimesCircle,
faTrash,
faCheckCircle,
faStream,
faSave,
faExclamationCircle,
faBullhorn,
faArrowsAltV,
faUnlink,
faQuestionCircle,
faImages,
faUpload,
);
export { FontAwesomeIcon };
export { FontAwesomeIcon }

View File

@@ -1,18 +1,14 @@
# 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.
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. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
4. Your language file should be filled in. You can translate now.
5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
6. Import your language file in `src/i18n.js` and add it to `languageList` constant.
7. 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. 😏

181
src/languages/bg-BG.js Normal file
View File

@@ -0,0 +1,181 @@
export default {
languageName: "Български",
checkEverySecond: "Проверявай на всеки {0} секунди.",
retryCheckEverySecond: "Повторен опит на всеки {0} секунди.",
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие",
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.",
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
passwordNotMatchMsg: "Повторената парола не съвпада.",
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.",
keywordDescription: "Търсете ключова дума в обикновен html или JSON отговор - чувствителна е към регистъра",
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?",
deleteNotificationMsg: "Наистина ли желаете да изтриете известието за всички монитори?",
resoverserverDescription: "Cloudflare е сървърът по подразбиране, можете да промените сървъра по всяко време.",
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате",
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?",
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да изключите известието за всеки отделен монитор.",
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?",
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?",
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?",
importHandleDescription: "Изберете 'Пропусни съществуващите', ако искате да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.",
confirmImportMsg: "Сигурни ли сте за импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.",
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи",
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.",
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?",
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?",
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: "Повторни опити",
"Heartbeat Retry Interval": "Честота на повторните опити",
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": "Повторете паролата",
"Import Backup": "Импорт на архив",
"Export Backup": "Експорт на архив",
Export: "Експорт",
Import: "Импорт",
respTime: "Време за отговор (ms)",
notAvailableShort: "Няма",
"Default enabled": "Включен по подразбиране",
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
Create: "Създай",
"Clear Data": "Изчисти данни",
Events: "Събития",
Heartbeats: "Проверки",
"Auto Get": "Автоматияно получаване",
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.",
backupDescription2: "PS: Данни за история и събития не са включени.",
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.",
alertNoFile: "Моля, изберете файл за импортиране.",
alertWrongFileType: "Моля, изберете JSON файл.",
"Clear all statistics": "Изчисти всички статистики",
"Skip existing": "Пропусни съществуващите",
Overwrite: "Презапиши",
Options: "Опции",
"Keep both": "Запази двете",
"Verify Token": "Проверка на токен код",
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включи 2FA",
"Disable 2FA": "Изключи 2FA",
"2FA Settings": "Настройки 2FA",
"Two Factor Authentication": "Двуфакторно удостоверяване",
Active: "Активно",
Inactive: "Неактивно",
Token: "Токен код",
"Show URI": "Покажи URI",
Tags: "Етикети",
"Add New below or Select...": "Добави нов по-долу или избери...",
"Tag with this name already exist.": "Етикет с това име вече съществува.",
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.",
color: "цвят",
"value (optional)": "стойност (по желание)",
Gray: "Сиво",
Red: "Червено",
Orange: "Оранжево",
Green: "Зелено",
Blue: "Синьо",
Indigo: "Индиго",
Purple: "Лилаво",
Pink: "Розово",
"Search...": "Търси...",
"Avg. Ping": "Ср. пинг",
"Avg. Response": "Ср. отговор",
"Entry Page": "Основна страница",
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.",
"No Services": "Няма Услуги",
"All Systems Operational": "Всички системи функционират",
"Partially Degraded Service": "Частично влошена услуга",
"Degraded Service": "Влошена услуга",
"Add Group": "Добави група",
"Add a monitor": "Добави монитор",
"Edit Status Page": "Редактирай статус страница",
"Go to Dashboard": "Към Таблото",
};

View File

@@ -17,8 +17,8 @@ export default {
Down: "Inaktiv",
Pending: "Afventer",
Unknown: "Ukendt",
Pause: "Pause",
pauseDashboardHome: "Pauset",
Pause: "Stands",
pauseDashboardHome: "Standset",
Name: "Navn",
Status: "Status",
DateTime: "Dato / Tid",
@@ -36,8 +36,7 @@ export default {
hour: "Timer",
"-hour": "-Timer",
checkEverySecond: "Tjek hvert {0} sekund",
"Avg.": "Gennemsnit",
Response: " Respons",
Response: "Respons",
Ping: "Ping",
"Monitor Type": "Overvåger Type",
Keyword: "Nøgleord",
@@ -103,10 +102,81 @@ export default {
"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?",
pauseMonitorMsg: "Er du sikker på, at du vil standse 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"
}
respTime: "Resp. Tid (ms)",
notAvailableShort: "N/A",
Create: "Opret",
clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?",
clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?",
confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?",
"Clear Data": "Ryd Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto-hent",
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
"Default enabled": "Standard aktiveret",
"Also apply to existing monitors": "Anvend også på eksisterende overvågere",
Export: "Eksport",
Import: "Import",
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.",
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
alertNoFile: "Vælg en fil der skal importeres.",
alertWrongFileType: "Vælg venligst en JSON-fil.",
twoFAVerifyLabel: "Indtast venligst dit token for at bekræfte, at 2FA fungerer",
tokenValidSettingsMsg: "Token er gyldigt! Du kan nu gemme 2FA -indstillingerne.",
confirmEnableTwoFAMsg: "Er du sikker på at du vil aktivere 2FA?",
confirmDisableTwoFAMsg: "Er du sikker på at du vil deaktivere 2FA?",
"Apply on all existing monitors": "Anvend på alle eksisterende overvågere",
"Verify Token": "Verificere Token",
"Setup 2FA": "Opsæt 2FA",
"Enable 2FA": "Aktiver 2FA",
"Disable 2FA": "Deaktiver 2FA",
"2FA Settings": "2FA Indstillinger",
"Two Factor Authentication": "To-Faktor Autentificering",
Active: "Aktive",
Inactive: "Inaktive",
Token: "Token",
"Show URI": "Vis URI",
"Clear all statistics": "Ryd alle Statistikker",
retryCheckEverySecond: "Prøv igen hvert {0} sekund.",
importHandleDescription: "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
confirmImportMsg: "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
"Heartbeat Retry Interval": "Heartbeat Gentagelsesinterval",
"Import Backup": "Importer Backup",
"Export Backup": "Eksporter Backup",
"Skip existing": "Spring over eksisterende",
Overwrite: "Overskriv",
Options: "Valgmuligheder",
"Keep both": "Behold begge",
Tags: "Tags",
"Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
"Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
"Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
color: "farve",
"value (optional)": "værdi (valgfri)",
Gray: "Grå",
Red: "Rød",
Orange: "Orange",
Green: "Grøn",
Blue: "Blå",
Indigo: "Indigo",
Purple: "Lilla",
Pink: "Pink",
"Search...": "Søg...",
"Avg. Ping": "Gns. Ping",
"Avg. Response": "Gns. Respons",
"Entry Page": "Entry Side",
"statusPageNothing": "Intet her, tilføj venligst en Gruppe eller en Overvåger.",
"No Services": "Ingen Tjenester",
"All Systems Operational": "Alle Systemer i Drift",
"Partially Degraded Service": "Delvist Forringet Service",
"Degraded Service": "Forringet Service",
"Add Group": "Tilføj Gruppe",
"Add a monitor": "Tilføj en Overvåger",
"Edit Status Page": "Rediger Statusside",
"Go to Dashboard": "Gå til Dashboard",
};

View File

@@ -36,8 +36,7 @@ export default {
hour: "Stunde",
"-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden",
"Avg.": "Durchschn. ",
Response: " Antwortzeit",
Response: "Antwortzeit",
Ping: "Ping",
"Monitor Type": "Monitor Typ",
Keyword: "Schlüsselwort",
@@ -104,9 +103,79 @@ export default {
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?",
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?",
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?",
"Clear Data": "Lösche Daten",
Events: "Ereignisse",
Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin Konto",
"Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type",
Export: "Export",
Import: "Import",
respTime: "Antw. Zeit (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert",
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
Create: "Erstellen",
"Auto Get": "Auto Get",
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
"Clear all statistics": "Lösche alle Statistiken",
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
"Skip existing": "Vorhandene überspringen",
Overwrite: "Überschreiben",
Options: "Optionen",
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
"Keep both": "Beide behalten",
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
"Verify Token": "Token verifizieren",
"Setup 2FA": "2FA Einrichten",
"Enable 2FA": "2FA Aktivieren",
"Disable 2FA": "2FA deaktivieren",
"2FA Settings": "2FA Einstellungen",
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
"Two Factor Authentication": "Zwei Faktor Authentifizierung",
Active: "Aktiv",
Inactive: "Inaktiv",
Token: "Token",
"Show URI": "URI Anzeigen",
Tags: "Tags",
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...",
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.",
color: "Farbe",
"value (optional)": "Wert (Optional)",
Gray: "Grau",
Red: "Rot",
Orange: "Orange",
Green: "Grün",
Blue: "Blau",
Indigo: "Indigo",
Purple: "Lila",
Pink: "Pink",
"Search...": "Suchen...",
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall",
retryCheckEverySecond: "Versuche alle {0} Sekunden",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Avg. Ping": "Durchsch. Ping",
"Avg. Response": "Durchsch. Antwort",
"Entry Page": "Einstiegsseite",
statusPageNothing: "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
"No Services": "Keine Dienste",
"All Systems Operational": "Alle Systeme Betriebsbereit",
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
"Degraded Service": "Eingeschränkter Dienst",
"Add Group": "Gruppe hinzufügen",
"Add a monitor": "Monitor hinzufügen",
"Edit Status Page": "Bearbeite Statusseite",
"Go to Dashboard": "Gehe zum Dashboard",
};

View File

@@ -1,7 +1,7 @@
export default {
languageName: "English",
checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg. ",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
@@ -16,6 +16,16 @@ export default {
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?",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
Settings: "Settings",
Dashboard: "Dashboard",
"New Update": "New Update",
@@ -60,6 +70,7 @@ export default {
Port: "Port",
"Heartbeat Interval": "Heartbeat Interval",
Retries: "Retries",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects",
@@ -107,6 +118,64 @@ export default {
"Last Result": "Last Result",
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
Export: "Export",
Import: "Import",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
"Default enabled": "Default enabled",
"Apply on all existing monitors": "Apply on all existing monitors",
Create: "Create",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
"Clear all statistics": "Clear all Statistics",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

View File

@@ -1,7 +1,6 @@
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.",
@@ -32,7 +31,7 @@ export default {
Up: "Funcional",
Down: "Caído",
Pending: "Pendiente",
Unknown: "Desconociso",
Unknown: "Desconocido",
Pause: "Pausa",
Name: "Nombre",
Status: "Estado",
@@ -108,5 +107,76 @@ export default {
"Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

182
src/languages/et-EE.js Normal file
View File

@@ -0,0 +1,182 @@
export default {
languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
passwordNotMatchMsg: "Salasõnad ei kattu.",
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
pauseDashboardHome: "Seismas",
deleteMonitorMsg: "Kas soovid eemaldada seire?",
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded",
Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel",
Appearance: "Välimus",
Theme: "Teema",
General: "Üldine",
Version: "Versioon",
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
List: "Nimekiri",
Add: "Lisa",
"Add New Monitor": "Seire lisamine",
"Quick Stats": "Ülevaade",
Up: "Töökorras",
Down: "Rikkis",
Pending: "Määramisel",
Unknown: "Teadmata",
Pause: "Seiskamine",
Name: "Nimi",
Status: "Olek",
DateTime: "Kuupäev",
Message: "Tulemus",
"No important events": "Märkimisväärsed juhtumid puuduvad.",
Resume: "Taasta",
Edit: "Muutmine",
Delete: "Eemalda",
Current: "Hetkeseisund",
Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine",
days: "päeva",
day: "päev",
"-day": "-päev",
hour: "tund",
"-hour": "-tund",
Response: "Vastus",
Ping: "Ping",
"Monitor Type": "Seire tüüp",
Keyword: "Võtmesõna",
"Friendly Name": "Sõbralik nimi",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Tukse sagedus",
Retries: "Korduskatsed",
Advanced: "Rohkem",
"Upside Down Mode": "Tagurpidi seire",
"Max. Redirects": "Max. ümbersuunamine",
"Accepted Status Codes": "Kõlblikud HTTP koodid",
Save: "Salvesta",
Notifications: "Teavitused",
"Not available, please setup.": "Ühtegi teavitusteenust pole saadaval.",
"Setup Notification": "Lisa teavitusteenus",
Light: "hele",
Dark: "tume",
Auto: "automaatne",
"Theme - Heartbeat Bar": "Teemasäte — tuksete riba",
Normal: "tavaline",
Bottom: "all",
None: "puudub",
Timezone: "Ajatsoon",
"Search Engine Visibility": "Otsimootorite ligipääs",
"Allow indexing": "Luba indekseerimine",
"Discourage search engines from indexing site": "Keela selle saidi indekseerimine otsimootorite poolt",
"Change Password": "Muuda parooli",
"Current Password": "praegune parool",
"New Password": "uus parool",
"Repeat New Password": "korda salasõna",
"Update Password": "Uuenda salasõna",
"Disable Auth": "Lülita autentimine välja",
"Enable Auth": "Lülita autentimine sisse",
Logout: "Logi välja",
Leave: "Lahku",
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",
Confirm: "Kinnita",
Yes: "Jah",
No: "Ei",
Username: "kasutajanimi",
Password: "parool",
"Remember me": "Mäleta mind",
Login: "Logi sisse",
"No Monitors, please": "Seired puuduvad.",
"add one": "Lisa esimene",
"Notification Type": "Teavituse tüüp",
Email: "e-posti aadress",
Test: "Saada prooviteavitus",
"Certificate Info": "Sertifikaadi teave",
"Resolver Server": "Server, mis vastab DNS päringutele.",
"Resource Record Type": "DNS kirje tüüp",
"Last Result": "Viimane",
"Create your admin account": "Admininstraatori konto loomine",
"Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A",
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
Export: "Eksport",
Import: "Import",
"Default enabled": "Kasuta vaikimisi",
"Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
Create: "Loo konto",
"Clear Data": "Eemalda andmed",
Events: "Sündmused",
Heartbeats: "Tuksed",
"Auto Get": "Hangi automaatselt",
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
alertNoFile: "Palun lisa fail, mida importida.",
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

View File

@@ -36,7 +36,6 @@ export default {
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",
@@ -66,7 +65,7 @@ export default {
"Theme - Heartbeat Bar": "Voir les services surveillés",
Normal: "Général",
Bottom: "En dessous",
None: "Non",
None: "Rien",
Timezone: "Fuseau Horaire",
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
@@ -108,5 +107,76 @@ export default {
"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"
}
notAvailableShort: "N/A",
Create: "Créer",
clearEventsMsg: "Êtes-vous sûr de vouloir supprimer tous les événements pour cette sonde ?",
clearHeartbeatsMsg: "Êtes-vous sûr de vouloir supprimer tous les vérifications pour cette sonde ? Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "tes-vous sûr de vouloir supprimer tous les statistiques ?",
"Clear Data": "Effacer les données",
Events: "Evénements",
Heartbeats: "Vérfications",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "Pour chaque nouvelle sonde, cette notification sera activée par défaut. Vous pouvez toujours désactiver la notification séparément pour chaque sonde.",
"Default enabled": "Activé par défaut",
"Also apply to existing monitors": "S'applique également aux sondes existantes",
Export: "Exporter",
Import: "Importer",
backupDescription: "Vous pouvez sauvegarder toutes les sondes et toutes les notifications dans un fichier JSON.",
backupDescription2: "PS: Les données relatives à l'historique et aux événements ne sont pas incluses.",
backupDescription3: "Les données sensibles telles que les jetons de notification sont incluses dans le fichier d'exportation, veuillez les conserver soigneusement.",
alertNoFile: "Veuillez sélectionner un fichier à importer.",
alertWrongFileType: "Veuillez sélectionner un fichier JSON à importer.",
twoFAVerifyLabel: "Veuillez saisir votre jeton pour vérifier que le système 2FA fonctionne.",
tokenValidSettingsMsg: "Le jeton est valide ! Vous pouvez maintenant sauvegarder les paramètres 2FA.",
confirmEnableTwoFAMsg: "Êtes-vous sûr de vouloir activer le 2FA ?",
confirmDisableTwoFAMsg: "Êtes-vous sûr de vouloir désactiver le 2FA ?",
"Apply on all existing monitors": "Appliquer sur toutes les sondes existantes",
"Verify Token": "Vérifier le jeton",
"Setup 2FA": "Configurer 2FA",
"Enable 2FA": "Activer 2FA",
"Disable 2FA": "Désactiver 2FA",
"2FA Settings": "Paramètres 2FA",
"Two Factor Authentication": "Authentification à deux facteurs",
Active: "Actif",
Inactive: "Inactif",
Token: "Jeton",
"Show URI": "Afficher l'URI",
"Clear all statistics": "Effacer touutes les statistiques",
retryCheckEverySecond: "Réessayer toutes les {0} secondes.",
importHandleDescription: "Choisissez 'Ignorer l'existant' si vous voulez ignorer chaque sonde ou notification portant le même nom. L'option 'Écraser' supprime tous les sondes et notifications existantes.",
confirmImportMsg: "Êtes-vous sûr d'importer la sauvegarde ? Veuillez vous assurer que vous avez sélectionné la bonne option d'importation.",
"Heartbeat Retry Interval": "Réessayer l'intervale de vérification",
"Import Backup": "Importation de la sauvegarde",
"Export Backup": "Exportation de la sauvegarde",
"Skip existing": "Sauter l'existant",
Overwrite: "Ecraser",
Options: "Options",
"Keep both": "Garder les deux",
Tags: "Étiquettes",
"Add New below or Select...": "Ajouter nouveau ci-dessous ou sélectionner...",
"Tag with this name already exist.": "Une étiquette portant ce nom existe déjà.",
"Tag with this value already exist.": "Une étiquette avec cette valeur existe déjà.",
color: "couleur",
"value (optional)": "valeur (facultatif)",
Gray: "Gris",
Red: "Rouge",
Orange: "Orange",
Green: "Vert",
Blue: "Bleu",
Indigo: "Indigo",
Purple: "Violet",
Pink: "Rose",
"Search...": "Rechercher...",
"Avg. Ping": "Ping moyen",
"Avg. Response": "Réponse moyenne",
"Entry Page": "Page d'accueil",
"statusPageNothing": "Rien ici, veuillez ajouter un groupe ou une sonde.",
"No Services": "Aucun service",
"All Systems Operational": "Tous les systèmes sont opérationnels",
"Partially Degraded Service": "Service partiellement dégradé",
"Degraded Service": "Service dégradé",
"Add Group": "Ajouter un groupe",
"Add a monitor": "Ajouter une sonde",
"Edit Status Page": "Modifier la page de statut",
"Go to Dashboard": "Accéder au tableau de bord",
};

181
src/languages/it-IT.js Normal file
View File

@@ -0,0 +1,181 @@
export default {
languageName: "Italiano (Italian)",
checkEverySecond: "controlla ogni {0} secondi",
retryCheckEverySecond: "Riprova ogni {0} secondi.",
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
maxRedirectDescription: "Numero massimo di redirezionamenti consentito. Per disabilitare impostare \"0\".",
acceptedStatusCodesDescription: "Inserire i codici di stato considerati come risposte corrette.",
passwordNotMatchMsg: "La password non coincide.",
notificationDescription: "Assegnare la notifica a uno o più elementi monitorati per metterla in funzione.",
keywordDescription: "Cerca la parola chiave nella risposta in html o JSON e fai distinzione tra maiuscole e minuscole",
pauseDashboardHome: "In Pausa",
deleteMonitorMsg: "Si è certi di voler eliminare questo monitoraggio?",
deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?",
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
enableDefaultNotificationDescription: "Per ogni nuovo monitoraggio questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica separatamente per ogni monitoraggio.",
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
importHandleDescription: "Selezionare 'Ignora gli esistenti' si vuole ignorare l'importazione dei monitoraggi o delle notifiche con lo stesso nome. 'Sovrascrivi' eliminerà ogni monitoraggio e notifica esistente.",
confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.",
twoFAVerifyLabel: "Scrivi il token per verificare che l'autenticazione a due fattori funzioni",
tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.",
confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?",
confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?",
Settings: "Impostazioni",
Dashboard: "Cruscotto",
"New Update": "Nuovo Aggiornamento Disponibile",
Language: "Lingua",
Appearance: "Aspetto",
Theme: "Tema",
General: "Generali",
Version: "Versione",
"Check Update On GitHub": "Controlla aggiornamenti su GitHub",
List: "Lista",
Add: "Aggiungi",
"Add New Monitor": "Aggiungi un nuovo monitoraggio",
"Quick Stats": "Statistiche rapide",
Up: "Su",
Down: "Giù",
Pending: "Pendente",
Unknown: "Sconosciuti",
Pause: "Metti in Pausa",
Name: "Nome",
Status: "Stato",
DateTime: "Data e Ora",
Message: "Messaggio",
"No important events": "Nessun evento importante",
Resume: "Riprendi",
Edit: "Modifica",
Delete: "Elimina",
Current: "Corrente",
Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato",
days: "giorni",
day: "giorno",
"-day": "-giorni",
hour: "ora",
"-hour": "-ore",
Response: "Risposta",
Ping: "Ping",
"Monitor Type": "Tipo di Monitoraggio",
Keyword: "Parola chiave",
"Friendly Name": "Nome Amichevole",
URL: "URL",
Hostname: "Nome Host",
Port: "Porta",
"Heartbeat Interval": "Intervallo di controllo",
Retries: "Tentativi",
"Heartbeat Retry Interval": "Intervallo tra un tentativo di controllo e l'altro",
Advanced: "Avanzate",
"Upside Down Mode": "Modalità capovolta",
"Max. Redirects": "Reindirizzamenti massimi",
"Accepted Status Codes": "Codici di stato accettati",
Save: "Salva",
Notifications: "Notifiche",
"Not available, please setup.": "Non disponibili, da impostare.",
"Setup Notification": "Imposta le notifiche",
Light: "Chiaro",
Dark: "Scuro",
Auto: "Automatico",
"Theme - Heartbeat Bar": "Tema - Barra di Stato",
Normal: "Normale",
Bottom: "Sotto",
None: "Nessuna",
Timezone: "Fuso Orario",
"Search Engine Visibility": "Visibilità ai motori di ricerca",
"Allow indexing": "Permetti l'indicizzazione",
"Discourage search engines from indexing site": "Scoraggia l'indicizzazione da parte dei motori di ricerca",
"Change Password": "Cambio Password",
"Current Password": "Password Corrente",
"New Password": "Nuova Password",
"Repeat New Password": "Ripetere la nuova Password",
"Update Password": "Modifica Password",
"Disable Auth": "Disabilita l'autenticazione",
"Enable Auth": "Abilita Autenticazione",
Logout: "Esci",
Leave: "Annulla",
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",
Confirm: "Conferma",
Yes: "Sì",
No: "No",
Username: "Nome Utente",
Password: "Password",
"Remember me": "Ricordami",
Login: "Accesso",
"No Monitors, please": "Nessun monitoraggio, cortesemente",
"add one": "aggiungerne uno",
"Notification Type": "Tipo di notifica",
Email: "E-mail",
Test: "Prova",
"Certificate Info": "Informazioni sul certificato",
"Resolver Server": "Server DNS",
"Resource Record Type": "Tipo di Resource Record",
"Last Result": "Ultimo risultato",
"Create your admin account": "Crea l'account amministratore",
"Repeat Password": "Ripeti Password",
"Import Backup": "Importa Backup",
"Export Backup": "Esporta Backup",
Export: "Esporta",
Import: "Importa",
respTime: "Tempo di Risposta (ms)",
notAvailableShort: "N/D",
"Default enabled": "Abilitato di default",
"Apply on all existing monitors": "Applica su tutti i monitoraggi",
Create: "Crea",
"Clear Data": "Cancella dati",
Events: "Eventi",
Heartbeats: "Controlli",
"Auto Get": "Auto Get",
backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.",
backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi.",
backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.",
alertNoFile: "Selezionare il file da importare.",
alertWrongFileType: "Selezionare un file JSON.",
"Clear all statistics": "Pulisci tutte le statistiche",
"Skip existing": "Ignora gli esistenti",
Overwrite: "Sovrascrivi",
Options: "Opzioni",
"Keep both": "Mantieni entrambi",
"Verify Token": "Verifica Token",
"Setup 2FA": "Imposta l'autenticazione a due fattori",
"Enable 2FA": "Abilita l'autenticazione a due fattori",
"Disable 2FA": "Disabilita l'autenticazione a due fattori",
"2FA Settings": "Impostazioni autenticazione a due fattori",
"Two Factor Authentication": "Autenticazione a due fattori",
Active: "Attivata",
Inactive: "Disattivata",
Token: "Token",
"Show URI": "Mostra URI",
Tags: "Etichette",
"Add New below or Select...": "Aggiungine una oppure scegli...",
"Tag with this name already exist.": "Un'etichetta con questo nome già esiste.",
"Tag with this value already exist.": "Un'etichetta con questo valore già esiste.",
color: "colori",
"value (optional)": "valore (opzionale)",
Gray: "Grigio",
Red: "Rosso",
Orange: "Arancione",
Green: "Verde",
Blue: "Blu",
Indigo: "Indigo",
Purple: "Viola",
Pink: "Rosa",
"Search...": "Cerca...",
"Avg. Ping": "Ping medio",
"Avg. Response": "Risposta media",
"Entry Page": "Entry Page",
"statusPageNothing": "Non c'è nulla qui, aggiungere un gruppo oppure un monitoraggio.",
"No Services": "Nessun Servizio",
"All Systems Operational": "Tutti i sistemi sono operativi",
"Partially Degraded Service": "Servizio parzialmente degradato",
"Degraded Service": "Servizio degradato",
"Add Group": "Aggiungi Gruppo",
"Add a monitor": "Aggiungi un monitoraggio",
"Edit Status Page": "Modifica pagina di stato",
"Go to Dashboard": "Vai al Cruscotto",
};

View File

@@ -1,7 +1,6 @@
export default {
languageName: "日本語",
checkEverySecond: "{0}秒ごとにチェックします。",
"Avg.": "平均 ",
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
@@ -108,5 +107,76 @@ export default {
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

View File

@@ -1,7 +1,6 @@
export default {
languageName: "한국어",
checkEverySecond: "{0} 초마다 체크해요.",
"Avg.": "평균 ",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
@@ -108,5 +107,76 @@ export default {
"Create your admin account": "관리자 계정 만들기",
"Repeat Password": "비밀번호 재입력",
respTime: "응답 시간 (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

View File

@@ -1,7 +1,6 @@
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.",
@@ -16,6 +15,14 @@ export default {
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?",
enableDefaultNotificationDescription: "Voor elke nieuwe monitor wordt deze melding standaard ingeschakeld. U kunt de melding nog steeds afzonderlijk uitschakelen voor elke monitor.",
clearEventsMsg: "Weet je zeker dat je alle evenementen voor deze monitor wilt verwijderen?",
clearHeartbeatsMsg: "Weet je zeker dat je alle heartbeats voor deze monitor wilt verwijderen?",
confirmClearStatisticsMsg: "Weet u zeker dat u alle statistieken wilt verwijderen?",
twoFAVerifyLabel: "Voer uw 2FA controle token in voor verificatie",
tokenValidSettingsMsg: "Token is geldig! U kunt nu de 2FA-instellingen opslaan.",
confirmEnableTwoFAMsg: "Weet je zeker dat je 2FA wilt inschakelen?",
confirmDisableTwoFAMsg: "Weet je zeker dat je 2FA wilt uitschakelen?",
Settings: "Instellingen",
Dashboard: "Dashboard",
"New Update": "Nieuwe update",
@@ -80,7 +87,7 @@ export default {
"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",
"Current Password": "Huidig wachtwoord",
"New Password": "Nieuw wachtwoord",
"Repeat New Password": "Herhaal nieuw wachtwoord",
"Update Password": "Vernieuw wachtwoord",
@@ -107,6 +114,69 @@ export default {
"Last Result": "Laatste resultaat",
"Create your admin account": "Maak uw beheerdersaccount aan",
"Repeat Password": "Herhaal wachtwoord",
Export: "Exporteren",
Import: "Importeren",
respTime: "resp. tijd (ms)",
notAvailableShort: "N.v.t."
}
notAvailableShort: "N.v.t.",
"Default enabled": "Default enabled",
"Apply on all existing monitors": "Pas toe op alle bestaande monitors",
Create: "Aanmaken",
"Clear Data": "Data wissen",
Events: "Gebeurtenissen",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "U kunt een back-up maken van alle monitoren en alle meldingen in een JSON-bestand.",
backupDescription2: "PS: Geschiedenis- en gebeurtenisgegevens zijn niet inbegrepen.",
backupDescription3: "Gevoelige gegevens zoals melding tokens zijn opgenomen in het exportbestand, houd het veilig opgeslagen.",
alertNoFile: "Selecteer een bestand om te importeren.",
alertWrongFileType: "Selecteer een JSON-bestand.",
"Verify Token": "Controleer token",
"Setup 2FA": "2FA instellingen",
"Enable 2FA": "Schakel 2FA in",
"Disable 2FA": "Schakel 2FA uit",
"2FA Settings": "2FA-instellingen",
"Two Factor Authentication": "Two Factor Authenticatie",
Active: "Actief",
Inactive: "Inactief",
"Also apply to existing monitors": "Also apply to existing monitors",
Token: "Token",
"Show URI": "Toon URI",
"Clear all statistics": "Wis alle statistieken",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

182
src/languages/pl.js Normal file
View File

@@ -0,0 +1,182 @@
export default {
languageName: "Polski",
checkEverySecond: "Sprawdzam co {0} sekund.",
retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",
maxRedirectDescription: "Maksymalna liczba przekierowań do wykonania. Ustaw na 0, aby wyłączyć przekierowania.",
acceptedStatusCodesDescription: "Wybierz kody stanu, które są uważane za udaną odpowiedź.",
passwordNotMatchMsg: "Powtórzone hasło nie pasuje.",
notificationDescription: "Proszę przypisać powiadomienie do monitora(ów), aby zadziałało.",
keywordDescription: "Wyszukiwanie słów kluczowych w zwykłym html lub odpowiedzi JSON. Wielkość liter ma znaczenie.",
pauseDashboardHome: "Pauza",
deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?",
deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?",
resoverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
rrtypeDescription: "Wybierz RR-Type który chcesz monitorować",
pauseMonitorMsg: "Czy na pewno chcesz wstrzymać?",
Settings: "Ustawienia",
Dashboard: "Panel",
"New Update": "Nowa aktualizacja",
Language: "Język",
Appearance: "Wygląd",
Theme: "Motyw",
General: "Ogólne",
Version: "Wersja",
"Check Update On GitHub": "Sprawdź aktualizację na GitHub.",
List: "Lista",
Add: "Dodaj",
"Add New Monitor": "Dodaj nowy monitor",
"Quick Stats": "Szybkie statystyki",
Up: "Online",
Down: "Offline",
Pending: "Oczekujący",
Unknown: "Nieznane",
Pause: "Pauza",
Name: "Nazwa",
Status: "Status",
DateTime: "Data i godzina",
Message: "Wiadomość",
"No important events": "Brak ważnych wydarzeń",
Resume: "Wznów",
Edit: "Edytuj",
Delete: "Usuń",
Current: "aktualny",
Uptime: "Czas pracy",
"Cert Exp.": "Wygaśnięcie certyfikatu",
days: "dni",
day: "dzień",
"-day": " dni",
hour: "godzina",
"-hour": " godziny",
Response: "Odpowiedź",
Ping: "Ping",
"Monitor Type": "Typ monitora",
Keyword: "Słowo kluczowe",
"Friendly Name": "Przyjazna nazwa",
URL: "URL",
Hostname: "Nazwa hosta",
Port: "Port",
"Heartbeat Interval": "Interwał bicia serca",
Retries: "Prób",
Advanced: "Zaawansowane",
"Upside Down Mode": "Tryb do góry nogami",
"Max. Redirects": "Maks. przekierowania",
"Accepted Status Codes": "Akceptowane kody statusu",
Save: "Zapisz",
Notifications: "Powiadomienia",
"Not available, please setup.": "Niedostępne, proszę skonfigurować.",
"Setup Notification": "Konfiguracja powiadomień",
Light: "Jasny",
Dark: "Ciemny",
Auto: "Automatyczny",
"Theme - Heartbeat Bar": "Motyw - pasek bicia serca",
Normal: "Normalne",
Bottom: "Na dole",
None: "Brak",
Timezone: "Strefa czasowa",
"Search Engine Visibility": "Widoczność w wyszukiwarce",
"Allow indexing": "Pozwól na indeksowanie",
"Discourage search engines from indexing site": "Zniechęcaj wyszukiwarki do indeksowania strony",
"Change Password": "Zmień hasło",
"Current Password": "Aktualne hasło",
"New Password": "Nowe hasło",
"Repeat New Password": "Powtórz nowe hasło",
"Update Password": "Zaktualizuj hasło",
"Disable Auth": "Wyłącz autoryzację",
"Enable Auth": "Włącz autoryzację ",
Logout: "Wyloguj się",
Leave: "Zostaw",
"I understand, please disable": "Rozumiem, proszę wyłączyć",
Confirm: "Potwierdź",
Yes: "Tak",
No: "Nie",
Username: "Nazwa użytkownika",
Password: "Hasło",
"Remember me": "Zapamiętaj mnie",
Login: "Zaloguj się",
"No Monitors, please": "Brak monitorów, proszę",
"add one": "dodaj jeden",
"Notification Type": "Typ powiadomienia",
Email: "Email",
Test: "Test",
"Certificate Info": "Informacje o certyfikacie",
"Resolver Server": "Server resolver",
"Resource Record Type": "Typ rekordu zasobów",
"Last Result": "Ostatni wynik",
"Create your admin account": "Utwórz swoje konto administratora",
"Repeat Password": "Powtórz hasło",
respTime: "Czas odp. (ms)",
notAvailableShort: "N/A",
Create: "Stwórz",
clearEventsMsg: "Jesteś pewien, że chcesz usunąć wszystkie monitory dla tej strony?",
clearHeartbeatsMsg: "Jesteś pewien, że chcesz usunąć wszystkie bicia serca dla tego monitora?",
confirmClearStatisticsMsg: "Jesteś pewien, że chcesz usunąć WSZYSTKIE statystyki?",
"Clear Data": "Usuń dane",
Events: "Wydarzenia",
Heartbeats: "Bicia serca",
"Auto Get": "Pobierz automatycznie",
enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.",
"Default enabled": "Domyślnie włączone",
"Also apply to existing monitors": "Również zastosuj do obecnych monitorów",
Export: "Eksportuj",
Import: "Importuj",
backupDescription: "Możesz wykonać kopię zapasową wszystkich monitorów i wszystkich powiadomień do pliku JSON.",
backupDescription2: "PS: Historia i dane zdarzeń nie są uwzględniane.",
backupDescription3: "Poufne dane, takie jak tokeny powiadomień, są zawarte w pliku eksportu, prosimy o ostrożne przechowywanie.",
alertNoFile: "Proszę wybrać plik do importu.",
alertWrongFileType: "Proszę wybrać plik JSON.",
twoFAVerifyLabel: "Proszę podaj swój token 2FA, aby sprawdzić czy 2FA działa",
tokenValidSettingsMsg: "Token jest poprawny! Możesz teraz zapisać ustawienia 2FA.",
confirmEnableTwoFAMsg: "Jesteś pewien że chcesz włączyć 2FA?",
confirmDisableTwoFAMsg: "Jesteś pewien że chcesz wyłączyć 2FA?",
"Apply on all existing monitors": "Zastosuj do wszystki obecnych monitorów",
"Verify Token": "Weryfikuj token",
"Setup 2FA": "Konfiguracja 2FA",
"Enable 2FA": "Włącz 2FA",
"Disable 2FA": "Wyłącz 2FA",
"2FA Settings": "Ustawienia 2FA",
"Two Factor Authentication": "Uwierzytelnienie dwuskładnikowe",
Active: "Włączone",
Inactive: "Wyłączone",
Token: "Token",
"Show URI": "Pokaż URI",
"Clear all statistics": "Wyczyść wszystkie statystyki",
retryCheckEverySecond: "Ponawiaj co {0} sekund.",
importHandleDescription: "Wybierz 'Pomiń istniejące', jeśli chcesz pominąć każdy monitor lub powiadomienie o tej samej nazwie. 'Nadpisz' spowoduje usunięcie każdego istniejącego monitora i powiadomienia.",
confirmImportMsg: "Czy na pewno chcesz zaimportować kopię zapasową? Upewnij się, że wybrałeś właściwą opcję importu.",
"Heartbeat Retry Interval": "Częstotliwość ponawiania bicia serca",
"Import Backup": "Importuj kopię zapasową",
"Export Backup": "Eksportuj kopię zapasową",
"Skip existing": "Pomiń istniejące",
Overwrite: "Nadpisz",
Options: "Opcje",
"Keep both": "Zachowaj oba",
Tags: "Tagi",
"Add New below or Select...": "Dodaj nowy poniżej lub wybierz...",
"Tag with this name already exist.": "Tag o tej nazwie już istnieje.",
"Tag with this value already exist.": "Tag o tej wartości już istnieje.",
color: "kolor",
"value (optional)": "wartość (opcjonalnie)",
Gray: "Szary",
Red: "Czerwony",
Orange: "Pomarańczowy",
Green: "Zielony",
Blue: "Niebieski",
Indigo: "Indygo",
Purple: "Fioletowy",
Pink: "Różowy",
"Search...": "Szukaj...",
"Avg. Ping": "Średni ping",
"Avg. Response": "Średnia odpowiedź",
"Entry Page": "Wejdź na stronę",
"statusPageNothing": "Nic tu nie ma, dodaj monitor lub grupę.",
"No Services": "Brak usług",
"All Systems Operational": "Wszystkie systemy działają",
"Partially Degraded Service": "Częściowy błąd usługi",
"Degraded Service": "Błąd usługi",
"Add Group": "Dodaj grupę",
"Add a monitor": "Dodaj monitoe",
"Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu",
};

182
src/languages/pt-BR.js Normal file
View File

@@ -0,0 +1,182 @@
export default {
languageName: "Português (Brasileiro)",
checkEverySecond: "Verificar cada {0} segundos.",
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.",
acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.",
passwordNotMatchMsg: "A senha repetida não corresponde.",
notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.",
keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas",
pauseDashboardHome: "Pausar",
deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?",
deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?",
resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.",
rrtypeDescription: "Selecione o RR-Type que você deseja monitorar",
pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?",
enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.",
clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?",
clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?",
confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?",
importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.",
twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando",
tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.",
confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?",
confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?",
Settings: "Configurações",
Dashboard: "Dashboard",
"New Update": "Nova Atualização",
Language: "Linguagem",
Appearance: "Aparência",
Theme: "Tema",
General: "Geral",
Version: "Versão",
"Check Update On GitHub": "Verificar atualização no Github",
List: "Lista",
Add: "Adicionar",
"Add New Monitor": "Adicionar novo monitor",
"Quick Stats": "Estatísticas rápidas",
Up: "On",
Down: "Off",
Pending: "Pendente",
Unknown: "Desconhecido",
Pause: "Pausar",
Name: "Nome",
Status: "Status",
DateTime: "Data hora",
Message: "Mensagem",
"No important events": "Nenhum evento importante",
Resume: "Resumo",
Edit: "Editar",
Delete: "Deletar",
Current: "Atual",
Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.",
days: "dias",
day: "dia",
"-day": "-dia",
hour: "hora",
"-hour": "-hora",
Response: "Resposta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
Keyword: "Palavra-Chave",
"Friendly Name": "Nome Amigável",
URL: "URL",
Hostname: "Hostname",
Port: "Porta",
"Heartbeat Interval": "Intervalo de Heartbeat",
Retries: "Novas tentativas",
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat",
Advanced: "Avançado",
"Upside Down Mode": "Modo de cabeça para baixo",
"Max. Redirects": "Redirecionamento Máx.",
"Accepted Status Codes": "Status Code Aceitáveis",
Save: "Salvar",
Notifications: "Notificações",
"Not available, please setup.": "Não disponível, por favor configure.",
"Setup Notification": "Configurar Notificação",
Light: "Claro",
Dark: "Escuro",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
Normal: "Normal",
Bottom: "Inferior",
None: "Nenhum",
Timezone: "Fuso horário",
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
"Allow indexing": "Permitir Indexação",
"Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site",
"Change Password": "Mudar senha",
"Current Password": "Senha atual",
"New Password": "Nova Senha",
"Repeat New Password": "Repetir Nova Senha",
"Update Password": "Atualizar Senha",
"Disable Auth": "Desativar Autenticação",
"Enable Auth": "Ativar Autenticação",
Logout: "Deslogar",
Leave: "Sair",
"I understand, please disable": "Eu entendo, por favor desative.",
Confirm: "Confirmar",
Yes: "Sim",
No: "Não",
Username: "Usuário",
Password: "Senha",
"Remember me": "Lembre-me",
Login: "Autenticar",
"No Monitors, please": "Nenhum monitor, por favor",
"add one": "adicionar um",
"Notification Type": "Tipo de Notificação",
Email: "Email",
Test: "Testar",
"Certificate Info": "Info. do Certificado ",
"Resolver Server": "Resolver Servidor",
"Resource Record Type": "Tipo de registro de aplicação",
"Last Result": "Último resultado",
"Create your admin account": "Crie sua conta de admin",
"Repeat Password": "Repita a senha",
"Import Backup": "Importar Backup",
"Export Backup": "Exportar Backup",
Export: "Exportar",
Import: "Importar",
respTime: "Tempo de Resp. (ms)",
notAvailableShort: "N/A",
"Default enabled": "Padrão habilitado",
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
Create: "Criar",
"Clear Data": "Limpar Dados",
Events: "Eventos",
Heartbeats: "Heartbeats",
"Auto Get": "Obter Automático",
backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.",
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.",
alertNoFile: "Selecione um arquivo para importar.",
alertWrongFileType: "Selecione um arquivo JSON.",
"Clear all statistics": "Limpar todas as estatísticas",
"Skip existing": "Pular existente",
Overwrite: "Sobrescrever",
Options: "Opções",
"Keep both": "Manter os dois",
"Verify Token": "Verificar Token",
"Setup 2FA": "Configurar 2FA",
"Enable 2FA": "Ativar 2FA",
"Disable 2FA": "Desativar 2FA",
"2FA Settings": "Configurações do 2FA ",
"Two Factor Authentication": "Autenticação e Dois Fatores",
Active: "Ativo",
Inactive: "Inativo",
Token: "Token",
"Show URI": "Mostrar URI",
Tags: "Tag",
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
color: "cor",
"value (optional)": "valor (opcional)",
Gray: "Cinza",
Red: "Vermelho",
Orange: "Laranja",
Green: "Verde",
Blue: "Azul",
Indigo: "Índigo",
Purple: "Roxo",
Pink: "Rosa",
"Search...": "Buscar...",
"Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status",
"Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
"No Services": "Nenhum Serviço",
"All Systems Operational": "Todos os Serviços Operacionais",
"Partially Degraded Service": "Serviço parcialmente degradado",
"Degraded Service": "Serviço Degradado",
"Add Group": "Adicionar Grupo",
"Add a monitor": "Adicionar um monitor",
"Edit Status Page": "Editar Página de Status",
"Go to Dashboard": "Ir para a dashboard",
};

View File

@@ -1,12 +1,11 @@
export default {
languageName: "Русский",
checkEverySecond: "Проверять каждые {0} секунд.",
"Avg.": "Средн. ",
checkEverySecond: "проверять каждые {0} секунд",
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
maxRedirectDescription: "Максимальное количество перенаправлений. Поставьте 0, чтобы отключить перенаправления.",
acceptedStatusCodesDescription: "Выберите коды статусов, которые должны считаться за успешный ответ.",
acceptedStatusCodesDescription: "Выберите коды статусов для определения доступности сервиса.",
passwordNotMatchMsg: "Повтор пароля не совпадает.",
notificationDescription: "Привяжите уведомления к мониторам.",
keywordDescription: "Поиск слова в чистом HTML или в JSON-ответе (чувствительно к регистру)",
@@ -17,7 +16,7 @@ export default {
rrtypeDescription: "Выберите тип ресурсной записи, который вы хотите отслеживать",
pauseMonitorMsg: "Вы действительно хотите поставить на паузу?",
Settings: "Настройки",
Dashboard: "Панель",
Dashboard: "Панель мониторов",
"New Update": "Обновление",
Language: "Язык",
Appearance: "Внешний вид",
@@ -29,8 +28,8 @@ export default {
Add: "Добавить",
"Add New Monitor": "Новый монитор",
"Quick Stats": "Статистика",
Up: "Доступно",
Down: "Недоступно",
Up: "Доступен",
Down: "Н",
Pending: "Ожидание",
Unknown: "Неизвестно",
Pause: "Пауза",
@@ -62,7 +61,7 @@ export default {
Retries: "Попыток",
Advanced: "Дополнительно",
"Upside Down Mode": "Режим реверса статуса",
"Max. Redirects": "Макс. перенаправлений",
"Max. Redirects": "Макс. количество перенаправлений",
"Accepted Status Codes": "Допустимые коды статуса",
Save: "Сохранить",
Notifications: "Уведомления",
@@ -107,6 +106,82 @@ export default {
"Last Result": "Последний результат",
"Create your admin account": "Создайте аккаунт администратора",
"Repeat Password": "Повторите пароль",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
}
respTime: "Время ответа (мс)",
notAvailableShort: "Н/Д",
Create: "Создать",
clearEventsMsg: "Вы действительно хотите удалить всю статистику событий данного монитора?",
clearHeartbeatsMsg: "Вы действительно хотите удалить всю статистику опросов данного монитора?",
confirmClearStatisticsMsg: "Вы действительно хотите удалить ВСЮ статистику?",
"Clear Data": "Удалить статистику",
Events: "События",
Heartbeats: "Опросы",
"Auto Get": "Авто-получение",
enableDefaultNotificationDescription: "Для каждого нового монитора это уведомление будет включено по умолчанию. Вы всё ещё можете отключить уведомления в каждом мониторе отдельно.",
"Default enabled": "Использовать по умолчанию",
"Also apply to existing monitors": "Применить к существующим мониторам",
Export: "Резервная копия",
Import: "Восстановление",
backupDescription: "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла",
backupDescription2: "P.S. История и события сохранены не будут",
backupDescription3: "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте",
alertNoFile: "Выберите файл для импорта.",
alertWrongFileType: "Выберите JSON-файл.",
twoFAVerifyLabel: "Пожалуйста, введите свой токен, чтобы проверить работу 2FA",
tokenValidSettingsMsg: "Токен действителен! Теперь вы можете сохранить настройки 2FA.",
confirmEnableTwoFAMsg: "Вы действительно хотите включить 2FA?",
confirmDisableTwoFAMsg: "Вы действительно хотите выключить 2FA?",
"Apply on all existing monitors": "Применить ко всем существующим мониторам",
"Verify Token": "Проверить токен",
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включить 2FA",
"Disable 2FA": "Выключить 2FA",
"2FA Settings": "Настройки 2FA",
"Two Factor Authentication": "Двухфакторная аутентификация",
Active: "Активно",
Inactive: "Неактивно",
Token: "Токен",
"Show URI": "Показать URI",
"Clear all statistics": "Удалить всю статистику",
retryCheckEverySecond: "повторять каждые {0} секунд",
importHandleDescription: "Выберите \"Пропустить существующие\", если вы хотите пропустить каждый монитор или уведомление с таким же именем. \"Перезаписать\" удалит каждый существующий монитор или уведомление и добавит заново. Вариант \"Не проверять\" принудительно восстанавливает все мониторы и уведомления, даже если они уже существуют.",
confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.",
"Heartbeat Retry Interval": "Интервал повтора опроса",
"Import Backup": "Восстановление резервной копии",
"Export Backup": "Резервная копия",
"Skip existing": "Пропустить существующие",
Overwrite: "Перезаписать",
Options: "Опции",
"Keep both": "Не проверять",
Tags: "Теги",
"Add New below or Select...": "Добавить новый или выбрать...",
"Tag with this name already exist.": "Такой тег уже существует.",
"Tag with this value already exist.": "Тег с таким значением уже существует.",
color: "цвет",
"value (optional)": "значение (опционально)",
Gray: "Серый",
Red: "Красный",
Orange: "Оранжевый",
Green: "Зелёный",
Blue: "Синий",
Indigo: "Индиго",
Purple: "Пурпурный",
Pink: "Розовый",
"Search...": "Поиск...",
"Avg. Ping": "Средн. пинг",
"Avg. Response": "Средн. ответ",
"Entry Page": "Главная страница",
statusPageNothing: "Здесь пусто. Добавьте группу или монитор.",
"No Services": "Нет сервисов",
"All Systems Operational": "Все сервисы работают",
"Partially Degraded Service": "Сервисы частично не работают",
"Degraded Service": "Все сервисы не работают",
"Add Group": "Добавить группу",
"Add a monitor": "Добавить монитор",
"Edit Status Page": "Редактировать",
"Go to Dashboard": "Панель мониторов",
"Status Page": "Статус сервисов",
"Discard": "Отмена",
"Create Incident": "Создать инцидент",
"Switch to Dark Theme": "Тёмная тема",
"Switch to Light Theme": "Светлая тема",
};

View File

@@ -1,7 +1,6 @@
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.",
@@ -108,5 +107,76 @@ export default {
"Create your admin account": "Naprivi administratorski nalog",
"Repeat Password": "Ponovite lozinku",
respTime: "Vreme odg. (ms)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

View File

@@ -1,7 +1,6 @@
export default {
languageName: "Српски",
checkEverySecond: "Провери сваких {0} секунди.",
"Avg.": "Просечни ",
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
@@ -108,5 +107,76 @@ export default {
"Create your admin account": "Наприви администраторски налог",
"Repeat Password": "Поновите лозинку",
respTime: "Време одг. (мс)",
notAvailableShort: "N/A"
}
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
};

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