Compare commits

...

224 Commits

Author SHA1 Message Date
Louis Lam
6962e056ce Update to 1.19.0 2022-12-23 22:44:49 +08:00
Louis Lam
b7a898326e Merge pull request #2437 from MrEddX/bulgarian
Update bg-BG.js
2022-12-19 20:22:47 +08:00
MrEddX
a89be0e6d4 Update bg-BG.js
Translation Update
2022-12-19 13:37:42 +02:00
Louis Lam
b58b83541b Merge pull request #2428 from rbunpat/master
Update th-TH.js
2022-12-18 15:40:40 +08:00
Cyril59310
df4f91c20d Update EN language (#2429)
* Update EN language
2022-12-18 15:39:54 +08:00
Rachatat Bunpat
fc6b040a4e Update th-TH.js 2022-12-17 17:34:03 +07:00
Louis Lam
9838f36b50 Merge pull request #2418 from Johno95CZ/patch-1
Update cs-CZ.js
2022-12-17 17:44:55 +08:00
Louis Lam
a60072adbc Merge pull request #2423 from rbunpat/master
Update th-TH.js
2022-12-17 17:43:51 +08:00
Louis Lam
e7aeb6f6bf Merge pull request #2422 from Justman10000/master
Update
2022-12-17 17:43:25 +08:00
Rachatat Bunpat
06e570c52d Update th-TH.js 2022-12-16 19:15:14 +07:00
Justman10000
890b8f8333 Updated German 2022-12-16 10:42:41 +00:00
Louis Lam
df21f7da76 Check login for initServerTimezone 2022-12-16 12:56:40 +08:00
JohnoCZ
20c37a70f7 Update cs-CZ.js 2022-12-15 14:35:59 +01:00
Louis Lam
b75db27658 Fix lint 2022-12-15 13:57:28 +08:00
Louis Lam
765d8e1297 Fix #2318 2022-12-15 13:39:48 +08:00
Louis Lam
9dc2cc1f0d Copy timezone.js from dayjs 2022-12-15 13:05:04 +08:00
Louis Lam
2532becf61 Update to 1.19.0-beta.2 2022-12-14 23:37:00 +08:00
Louis Lam
6154776b34 Merge pull request #2409 from Justman10000/master
Update
2022-12-14 22:39:00 +08:00
Louis Lam
7e6b92203d Merge pull request #2406 from MrEddX/bulgarian
Bulgarian
2022-12-14 22:38:13 +08:00
Louis Lam
1da00d19fd Try to fix incorrect header check 2022-12-14 21:34:13 +08:00
Justman10000
da4bdab4f6 Updated german 2022-12-14 13:16:32 +00:00
MrEddX
86ab97ef56 Update bg-BG.js
Translation Update
2022-12-14 09:56:28 +02:00
MrEddX
345b0c1829 Update bg-BG.js
Fixed Style
2022-12-14 09:49:31 +02:00
MrEddX
2ac87fcea7 Update bg-BG.js
Fixed Typos
2022-12-14 09:26:43 +02:00
Cyril59310
4862bec965 Update Fr language + added variable for missing translation (#2395)
* Update FR language
2022-12-13 22:00:54 +08:00
Louis Lam
aa784fb3b2 Fix #2394 2022-12-13 16:48:23 +08:00
Louis Lam
466b403a96 Handle unexpected error of checkCertificate 2022-12-13 02:21:12 +08:00
Louis Lam
39987ba9ac Init server timezone 2022-12-12 22:57:57 +08:00
Louis Lam
3b87209e26 Add configurable dns cache 2022-12-12 17:19:22 +08:00
Louis Lam
e6dc0a0293 Slightly improve maintenance page's css on mobile 2022-12-12 16:06:17 +08:00
Louis Lam
5c5a339a36 Add links for status pages and maintenance for mobile (Fix #2257) 2022-12-12 15:54:46 +08:00
Louis Lam
3040bd41d9 Speed up armv7 build time of healthcheck by using go compiler cross-build feature in the host 2022-12-12 15:42:00 +08:00
Louis Lam
3b58fd3b3c Cache uptime 2022-12-11 21:33:26 +08:00
Louis Lam
bc86f8bb5f Reset busy_timeout to default 2022-12-11 20:25:15 +08:00
Louis Lam
ab5f6dc82c Fix css 2022-12-11 18:52:24 +08:00
Louis Lam
5176fd02c1 Fix healthcheck do not check https 2022-12-10 23:30:32 +08:00
Louis Lam
02b5cae577 Fix #2371 by left join maintenance_timeslot 2022-12-09 21:03:12 +08:00
Louis Lam
54aa7d5dca Merge remote-tracking branch 'origin/master' 2022-12-08 23:22:09 +08:00
Louis Lam
4cd5b5563f Fix #1145 2022-12-08 23:21:55 +08:00
Louis Lam
f1c30204b6 Merge pull request #2373 from saw303/master
Fixed some typos in the German translations 🇩🇪🇨🇭
2022-12-08 23:01:48 +08:00
Louis Lam
e478084ff9 Fix Uptime Kuma cannot be stopped 2022-12-08 19:13:47 +08:00
Louis Lam
2dff7dd380 Make dockerfile slightly clear and improve the build flow 2022-12-08 18:29:17 +08:00
Louis Lam
9bfa43100b Compile healthcheck.go 2022-12-08 18:29:17 +08:00
Louis Lam
ad5e1957b1 Deprecate healthcheck.js 2022-12-08 18:29:17 +08:00
Louis Lam
cc68ebca39 Convert healthcheck.js into go-lang 2022-12-08 18:29:17 +08:00
Silvio Wangler
aa27d976c2 Fixed some typos in the German translations 🇩🇪🇨🇭 2022-12-07 09:10:05 +01:00
Louis Lam
ecbc0f0477 Merge pull request #2369 from saw303/master
Added new language German (Switzerland)
2022-12-07 15:14:52 +08:00
Silvio Wangler
92caec95fe Added new language German (Switzerland) 2022-12-06 13:43:37 +01:00
Louis Lam
2c3abdc146 [stale-bot] Do not close pr 2022-12-05 20:11:28 +08:00
Louis Lam
b1170211b7 Update to 1.19.0-beta.1 2022-12-05 19:24:04 +08:00
Louis Lam
eadf2c810a Fix check version 2022-12-05 19:17:24 +08:00
Louis Lam
8aa97635ec Improve the clear filter button 2022-12-05 18:21:16 +08:00
Louis Lam
ee1a56caae Update /test-webhook and reevaluate sensitive fields 2022-12-05 18:18:19 +08:00
Louis Lam
e886df4788 Fix typo 2022-12-05 17:55:45 +08:00
Louis Lam
5196abfd36 Merge remote-tracking branch 'origin/master' into feat/add-auth-header-to-webhook-notification-#1919 2022-12-05 17:52:02 +08:00
Louis Lam
3e68cf2a1c Specify Accept-Encoding for axios request (Fix #2253) 2022-12-04 22:55:05 +08:00
Louis Lam
0ab82e6de3 Generate random nightly version 2022-12-04 22:44:50 +08:00
Louis Lam
8cdbe37f6f Update core-js 2022-12-04 21:41:08 +08:00
Louis Lam
28d13e198c Merge pull request #2350 from MrEddX/bulgarian
Bulgarian
2022-11-26 20:52:33 +08:00
MrEddX
14a062804e Update bg-BG.js
- Translation fixes
2022-11-25 22:00:52 +02:00
MrEddX
cf3e03ab40 Update bg-BG.js
- Added new  fields
- Translated new fields
- Fixed some typos
2022-11-25 21:56:00 +02:00
Louis Lam
191f3ad53b Merge pull request #2339 from jbrunner/fix-2296
Add socks5h support
2022-11-25 16:24:21 +08:00
Louis Lam
370d522920 Pin dependency of axios-ntlm to 1.3.0. As 1.3.1 causes error 2022-11-25 14:00:33 +08:00
Louis Lam
e0a1ad8a1c Update dependencies and drop start-server-watch-dev as it is unstable 2022-11-25 01:32:33 +08:00
Louis Lam
9720006934 Merge pull request #2151 from Computroniks/feature/#1817-add-mysql-monitor
Feat  Add MySQL/MariaDB monitor #1817
2022-11-25 01:27:40 +08:00
Joshua Brunner
cd270bd8b5 Add socks5h support
Add socks5h support as an extra option to not break previous socks5 implementation.
Allows to toggle between socks5 and socks5h explicit.

Fixes #2296
2022-11-22 11:18:16 +01:00
Louis Lam
bc3229828e Merge remote-tracking branch 'origin/master' 2022-11-20 00:11:38 +08:00
Louis Lam
a5f23b9839 Update Apprise from 1.0 to 1.2 2022-11-20 00:07:19 +08:00
Matthew Nickson
2052fa175f Merge branch 'master' into feature/#1817-add-mysql-monitor
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 19:04:14 +00:00
Matthew Nickson
15b63c82c3 Merge remote-tracking branch 'upstream/master' into feature/#1817-add-mysql-monitor
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 18:46:58 +00:00
Matthew Nickson
b053bc61ce Fixed MySQL monitor to close connection
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 18:34:02 +00:00
Jan Hartje
258ff56962 Merge branch 'louislam:master' into feat/add-auth-header-to-webhook-notification-#1919 2022-11-14 20:17:36 +01:00
Louis Lam
cb4e512dc6 Merge pull request #2316 from Dafnik/patch-fix-link-preview-description
Fix 'undefined' in link preview generation
2022-11-15 02:37:28 +08:00
Dafnik
4042c26390 Fix 'undefined' in link preview generation 2022-11-14 18:05:52 +01:00
Louis Lam
5da2315534 Merge pull request #2310 from Ealrang/patch-1
Update zh-CN.js
2022-11-14 22:02:43 +08:00
Ealrang
204015f1f5 Update zh-CN.js
Correct typos
2022-11-12 22:15:04 +08:00
Louis Lam
cc6d17d2e0 Merge pull request #1964 from minhhoangvn/feat/add-gRPC-protocol
Feat/add gRPC protocol
2022-11-11 12:34:35 +08:00
Louis Lam
68862c0b3f Fix Pushbullet do not handle general message correctly and fix name convention (Close #1890) 2022-11-01 20:27:40 +08:00
Louis Lam
fd15e7c2dc Merge remote-tracking branch 'origin/master' into ntfy-icon
# Conflicts:
#	server/notification-providers/ntfy.js
#	src/components/notifications/Ntfy.vue
#	src/languages/en.js
2022-10-31 17:10:20 +08:00
Louis Lam
5c4cf68937 Merge pull request #2260 from m-kiszka/smseagle
Added support for SMSEagle device API notifications
2022-10-31 17:03:34 +08:00
Louis Lam
214ddc264d Fix mistake 2022-10-29 23:40:09 +08:00
Louis Lam
2ea71839d1 Add npm run start-server-watch-dev for watching server code changes and restart (Node.js 19 only) 2022-10-29 23:37:05 +08:00
Louis Lam
54efde8185 Update socket.io and remove an useless event listener 2022-10-29 23:29:33 +08:00
Louis Lam
705124d4ac Merge pull request #2274 from 5idereal/patch-1
update zh-tw translation
2022-10-28 16:50:24 +08:00
5idereal
1cb6940590 update zh-tw translation 2022-10-28 15:50:00 +08:00
Louis Lam
0f8ad288f3 Merge pull request #2255 from b-reich/update-german-translation
Add german translations
2022-10-27 16:47:11 +08:00
Adam Stachowicz
434174d350 I18n PL update (#2264)
* [PL] Only formatting by ESLint for now

* Translate new i18n keys to polish + small grammar/typo fixes

* ESLint again after npm install...
2022-10-27 16:46:32 +08:00
minhhn3
3d1237ed53 fix: resolve conflict 2022-10-26 20:50:34 +07:00
minhhn3
b459408b10 fix: resolve conflict 2022-10-26 20:41:21 +07:00
Benjamin Reich
f04fe4d230 add new german translations 2022-10-25 10:50:58 +02:00
Louis Lam
e579610426 Merge pull request #2265 from Saibamen/patch-1
Add info about `npm install` to translators
2022-10-25 14:32:43 +08:00
Louis Lam
b115d3f8b9 Merge pull request #2266 from Saibamen/fix_linters
Fix 'dayjs' is never used warning
2022-10-25 14:23:59 +08:00
Adam Stachowicz
134b3b8ac1 Fix 'dayjs' is never used warning 2022-10-25 01:27:25 +02:00
Adam Stachowicz
e7e7751e7b Correct order 2022-10-24 23:38:38 +02:00
Adam Stachowicz
5cd58e6fa3 Add info about npm install to translators
Without this, you can have wrong indentation from ESLint
2022-10-24 23:36:21 +02:00
Marcin Kiszka
08763b700a Added support for SMSEagle device API notifications 2022-10-24 12:45:56 +02:00
Marcin Kiszka
781f855921 [empty commit] pull request for Added support for SMSEagle device API notifications 2022-10-24 12:44:29 +02:00
Louis Lam
9e81fe120f Merge pull request #2235 from dave9123/patch-3
Update id-ID.js
2022-10-20 15:57:39 +08:00
Dave
c0e67b6de9 Update id-ID.js 2022-10-17 20:09:25 +07:00
Louis Lam
a17084f75d Merge pull request #2229 from falentio/fix-id-lang
fix typos in id lang
2022-10-16 16:25:35 +08:00
Louis Lam
e4fe7b802a Update bg-BG.js #2228 2022-10-16 16:24:39 +08:00
falentio
5761bc9b90 fix typos in id lang 2022-10-16 13:49:25 +07:00
MrEddX
92ea019fd4 Update bg-BG.js
- Added new  fields
- Translated new fields
2022-10-16 07:21:23 +03:00
Cyril59310
a774b37369 Update FR language + fixed daytime error (#2226)
* Update FR language

* fix a daytime error + add for translation

* Update language file FR + fixed daytime error
2022-10-16 01:42:28 +08:00
Louis Lam
06755f249d Update to 1.19.0-beta.0 2022-10-15 21:02:56 +08:00
Louis Lam
64f84eb118 Update Details.vue's button styles 2022-10-15 21:01:48 +08:00
Louis Lam
afe12ccf24 A complete maintenance planning system (#1213)
A complete maintenance planning system from karelkryda/master
2022-10-15 20:54:28 +08:00
Louis Lam
6f4424de28 Remove unused language keys 2022-10-15 20:44:02 +08:00
Louis Lam
24cb212a37 Fix recurring 2022-10-15 20:15:50 +08:00
Louis Lam
d8a676abb6 Implement recurring day of month and day of week 2022-10-15 18:49:09 +08:00
Louis Lam
0b8d4cdaac Generate Next Timeslot for recurring interval 2022-10-15 17:17:26 +08:00
Louis Lam
268cbdbf8d Merge remote-tracking branch 'origin/master' into maintenance
# Conflicts:
#	server/server.js
#	src/components/settings/General.vue
2022-10-15 15:57:39 +08:00
Louis Lam
b60dde0b2d Update SQLite 2022-10-15 15:18:54 +08:00
Louis Lam
aecf95864e Add index for maintenance tables 2022-10-14 13:26:41 +08:00
Louis Lam
c662d259b0 Firefox Better Support #2206 2022-10-13 19:28:02 +08:00
Matthew Nickson
f459ea845c Added #2182 Add support for custom radius ports (#2197)
This commit adds support for the port to be specified when using the
radius monitor type. A check has been implemented to ensure that a null
value is not passed to the radius check function as could occur with
monitors that were created before this change was introduced. The
default port of 1812 is displayed when the user selects the radius
monitor in much the same way as the DNS port is handled. The port was
not included in the hostname in the form hostname:port in order to avoid
issues with IPv6 addresses and monitors that had been created before
this change was implemented.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-13 00:32:05 +08:00
Louis Lam
b24c75eec5 Merge pull request #2162 from UltraWelfare/fix-entry-page-redirect
Fixed entry route not redirecting correctly.
2022-10-13 00:28:34 +08:00
MagicFun1241
cb5f90aa89 Update Russian locale (#2218)
* Update ru-RU.js

* Remove duplicates

* Remove duplicates x2

* Revert to previous version for one translation

* Removed conflicting lines

* Remove conflicting 'Reverse Proxy' key
2022-10-13 00:27:50 +08:00
Louis Lam
edacff123b Add UTC in the serverTimezone dropdown 2022-10-12 22:13:07 +08:00
Louis Lam
2faf866e9e Implement generateTimeslot() for recurring interval type 2022-10-12 17:02:16 +08:00
Louis Lam
8cc3e4b7c1 Revert 2022-10-11 22:28:00 +08:00
Louis Lam
7b9766091e Revert testing 2022-10-11 22:18:09 +08:00
Louis Lam
d95e722658 Init dayjs for backend.spec.js 2022-10-11 22:09:18 +08:00
Louis Lam
39b6725163 Update maintenance tables 2022-10-11 21:48:43 +08:00
Louis Lam
dfb75c8afb Update status page's maintenance message 2022-10-11 20:56:48 +08:00
Louis Lam
e07aa982c3 WIP 2022-10-11 18:23:17 +08:00
Christian Meis
1e8a16504b Make icon optional for ntfy notificaation provider. Add Icon header to ntfy request only, if icon is actually defined. 2022-10-11 11:15:33 +02:00
Alexander Borzov
180d881ac1 Update ru-RU.js (#2217) 2022-10-11 15:35:08 +08:00
Louis Lam
2271ac4a5a Add info.serverTimezoneOffset and improve some styles 2022-10-11 14:52:47 +08:00
Louis Lam
f6bbd1ca67 Merge remote-tracking branch 'origin/master' into maintenance 2022-10-11 14:13:08 +08:00
Louis Lam
2ee8378814 Update to 1.18.5 2022-10-11 02:32:57 +08:00
Louis Lam
d5c02fc627 Update Maintenance list order by status 2022-10-11 01:59:47 +08:00
Louis Lam
c84de4d259 WIP: Add maintenance status 2022-10-11 01:45:30 +08:00
Louis Lam
c1ccaa7a9f WIP 2022-10-10 20:48:11 +08:00
Louis Lam
539683f8e9 Merge remote-tracking branch 'origin/master' into maintenance 2022-10-10 16:50:25 +08:00
Louis Lam
bd42450e55 Update vue-i18n from 9.1.9 to 9.2.2, force to use production version of vue-i18n in order to improve the performance 2022-10-10 16:23:32 +08:00
Louis Lam
71af23cf00 Fix #2207 2022-10-10 02:47:24 +08:00
Louis Lam
a577fba848 Change DateTime Range using serverTimezone 2022-10-10 02:28:03 +08:00
Louis Lam
a36f24d827 Add configurable server timezone 2022-10-09 20:59:58 +08:00
Louis Lam
b007681e67 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	server/model/monitor.js
#	server/model/status_page.js
#	src/languages/en.js
2022-10-09 19:26:00 +08:00
Louis Lam
07f9aafd7b Update to 1.18.4 2022-10-09 16:50:47 +08:00
Louis Lam
1c8631af8d Pin dependencies (#2205) 2022-10-09 16:02:47 +08:00
Louis Lam
0d2e02f569 Merge pull request #2190 from VasilisThePikachu/master
Adding Greek Language
2022-10-09 02:48:32 +08:00
Louis Lam
ad1a7c255f Drop exports.entryPage fully 2022-10-08 23:56:58 +08:00
Kevin Falentio
230e5110b1 Fix typo in id-ID language file (#2202) 2022-10-08 20:59:22 +08:00
Matthew Nickson
f67d7cdf3f Make update-language-files command more useful (#2198)
* [empty commit] pull request for Fix language update script

* Avoid mass changes with update-language-files

This commit updates the update-language-files script to prevent mass
changes as seen on a number of recent PRs where the contributer has
ran the script and comitted the results.
The script has been updated to now require the --language argument to
specify which language file to update. This ensures that only that file
is updated instead of all files. If the provided language code does not
already exist, a new file with that code is created. This should make
it easier to add new languages as you only need to pass the language
code to the script.
The base lang code is now also passed as an optional argument to negate
the need for a seperate script entry in package.json.
The script has been restructures into a couple of functions to make it
easier to understand.
ESlint now only checks the changed file instead of
them all in order to improve performance.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

* Updated translation docs for new command

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

* [update-language-files] Add cross-env-shell

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-08 15:01:47 +08:00
wellart
df2f536845 [id-ID.js] Fix some type and word (#2149)
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-08 02:15:03 +08:00
Vasilis The Pikachu
59e7aa74a3 Remove some double dots & fix backuprecommended 2022-10-07 17:51:45 +00:00
AnnAngela
43c1ec640c feat: 🌐 Update zh-cn and en translation (#2167)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-08 01:45:00 +08:00
Louis Lam
4f6dec41c6 Fix ntfy username should not be required 2022-10-07 20:46:43 +08:00
Vasilis The Pikachu
e29527e22f Update Greek 2022-10-07 11:33:29 +02:00
Vasilis The Pikachu
91f9e10c94 Merge branch 'louislam:master' into master 2022-10-07 10:55:42 +02:00
Vasilis The Pikachu
7d3cc002ea Added greek language 2022-10-07 08:55:12 +00:00
Louis Lam
6e07ed2081 Fix #2186 2022-10-07 15:02:19 +08:00
Louis Lam
60460442f8 Update to 1.18.3 2022-10-07 00:25:34 +08:00
Louis Lam
959ecc65ff Merge remote-tracking branch 'origin/master' 2022-10-06 23:28:29 +08:00
Louis Lam
c24b64921d Fix #2183 ntfy issue 2022-10-06 23:28:06 +08:00
janhartje
b879428a03 feat(notification): add additional Header to webhook 2022-10-05 17:48:07 +02:00
Ben Scobie
c28d8ddff9 Correctly handle multiple IPs in X-Forwarded-For (#2177)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-05 23:45:21 +08:00
janhartje
3c5de1c889 Merge branch 'master' of https://github.com/louislam/uptime-kuma into feat/add-auth-header-to-webhook-notification-#1919 2022-10-05 16:44:13 +02:00
CL0Pinette
528a615fb2 Add free.fr SMS notification provider (#2159) 2022-10-05 17:30:49 +08:00
Louis Lam
b993859926 Drop Jest e2e testing (#2174) 2022-10-05 14:26:30 +08:00
Louis Lam
a5c102e750 Update README.md 2022-10-05 14:19:50 +08:00
Cyril59310
64ba2dce24 Update FR language (#2173) 2022-10-05 13:57:38 +08:00
Louis Lam
e5145a209a Update cypress config 2022-10-05 13:29:03 +08:00
Louis Lam
12696dd53e Update README.md 2022-10-05 12:54:25 +08:00
Muhammed Hussein karimi
d565320f74 New Demo Server (#2172)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2022-10-05 12:53:58 +08:00
Sympatron GmbH
f1a9046193 Prevent terminal window from showing when using ping on Windows (#2152) 2022-10-04 23:30:19 +08:00
Louis Lam
afbc283423 Move Cypress directory and convert it to JavaScript (#2170) 2022-10-04 22:23:57 +08:00
George Tsomlektsis
3e699f8ac3 Fix linting errors. 2022-10-03 18:01:52 +03:00
George Tsomlektsis
b0d6b5b13d Fixed entry route not redirecting correctly when the status entry page changes slug. 2022-10-03 17:48:34 +03:00
Matthew Nickson
f9be918246 Add support for MySQL/MariaDB databases #1817
This commit adds support for monitoring MySQL and MariaDB database
servers. The mysql2 package was choosen over mysql as it provides a
promise wrapper and is reportedly faster than the original mysql package
whilst still maintaining the same API.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-02 01:52:53 +01:00
Matthew Nickson
314ae38f91 Changed name of SQL Server to avoid confusion
It appears that SQL Server causes some confusion among users as they
believe that it means any SQL database, not the Microsoft product SQL
Server. To avoid this issue, the display value has been changed to
Microsoft SQL Server. No backend changes have been made and it is still
stored as sqlserver in the database. This is only a frontent change.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-01 21:35:33 +01:00
Christian Meis
c03d911657 Update src/languages/en.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-09-28 11:39:13 +02:00
Christian Meis
e12642cf21 Fix double quotes in fallback for no icon url in ntfy notification provider settings 2022-09-28 10:24:56 +02:00
Christian Meis
618d904001 [empty commit] pull request for icon support in ntfy notification provider (fixes #2135) 2022-09-28 10:17:02 +02:00
Christian Meis
6f86236b63 Add support for icon to ntfy notification provider (requires minimum ntfy server version 1.28.0 and Android app 1.14.0, no iOS support as of today) 2022-09-28 10:13:18 +02:00
Louis Lam
204339fbed Make two functions to convert ISO 8601 <=> YYYY-MM-DD hh:mm:ss 2022-09-28 00:48:15 +08:00
Louis Lam
b1465c0282 - Maintenance standardize datetime format to YYYY-MM-DD hh:mm:ss
- Import dayjs extensions one time only
- Maintenance activeCondition centralize
2022-09-28 00:20:17 +08:00
Louis Lam
4002b9f577 [WIP] Checking maintenance time using maintenance_timeslot table 2022-09-27 20:44:44 +08:00
Louis Lam
3f63cb246b [WIP] Handle timezone offset for timeRange 2022-09-25 19:38:28 +08:00
Louis Lam
f11dfc8f43 [WIP] Add/Edit Maintenance with new UI and recurring 2022-09-24 19:18:24 +08:00
Louis Lam
9d99c39f30 Update Maintenance UI for recurring 2022-09-24 02:33:29 +08:00
Louis Lam
617ba49e6c Fix race condition of selectedStatusPagesOptions 2022-09-18 22:40:53 +08:00
Louis Lam
7853c2cc38 Update Maintenance UI 2022-09-18 22:34:05 +08:00
Louis Lam
f61c1c47aa Update Maintenance UI 2022-09-18 02:13:29 +08:00
Louis Lam
9fe07742ea Linting 2022-09-18 02:07:32 +08:00
Louis Lam
a29eae3213 Update Maintenance UI 2022-09-18 02:02:18 +08:00
Louis Lam
80698a58b8 Tidy up 2022-09-17 22:09:09 +08:00
Louis Lam
bb883e6fa0 Move maintenance under /maintenance 2022-09-17 22:00:11 +08:00
Louis Lam
120e578398 Move maintenance code to maintenance-socket-handler.js 2022-09-17 16:58:08 +08:00
Louis Lam
7017c2e625 Move maintenance code to maintenance-socket-handler.js 2022-09-17 16:54:21 +08:00
Louis Lam
2f67d26702 Merge manually, as this part had been moved 2022-09-17 16:20:10 +08:00
Louis Lam
90761cf831 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	server/database.js
#	server/model/monitor.js
#	server/routers/api-router.js
#	server/server.js
#	src/components/HeartbeatBar.vue
#	src/components/MonitorList.vue
#	src/icon.js
#	src/layouts/Layout.vue
#	src/mixins/datetime.js
#	src/mixins/socket.js
#	src/router.js
#	src/util.js
2022-09-17 16:12:57 +08:00
Louis Lam
68875c3091 Fix merging issue 2022-09-13 22:22:01 +08:00
Louis Lam
f35d7c0a1a Merge remote-tracking branch 'origin/master' into feat/add-gRPC-protocol
# Conflicts:
#	package-lock.json
2022-09-13 22:19:41 +08:00
minhhn3
3a90d246a4 fix: wrong type 2022-08-20 22:45:11 +07:00
minhhn3
6bb79597e8 fix: resolve merge conflict 2022-08-13 13:26:05 +07:00
minhhn3
34ab6142db fix: remove new space line 2022-08-08 19:38:43 +07:00
minhhn3
2232236a7a [empty commit] pull request for add gRPC protocol 2022-08-03 13:39:31 +07:00
Minh Hoàng
dcecd10c88 Feat/add gRPC protocol (#1)
* feat: added monitor with gRPC

Co-authored-by: minhhn3 <minhhn3@vng.com.vn>
2022-08-03 12:00:39 +07:00
Jan Hartje
af07c7f050 feat(notification): add Authorization Header option to backend 2022-07-18 16:04:27 +00:00
Jan Hartje
95dba6dcaf feat(notification): add Authorization Header option to frontend 2022-07-18 16:04:18 +00:00
Jan Hartje
90c2bf7c94 [empty commit] pull request for #1919 2022-07-18 15:56:53 +00:00
Karel Krýda
fa777c5bc0 Update server/server.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:32:42 +02:00
Karel Krýda
6d0683b055 Update server/routers/api-router.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:32:19 +02:00
Karel Krýda
25262cfb91 Update server/model/monitor.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:31:45 +02:00
Louis Lam
7a46b44d25 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	src/components/HeartbeatBar.vue
2022-05-18 19:49:54 +08:00
Karel Krýda
42f931f6cf Merge branch 'master' into master 2022-05-09 10:28:14 +02:00
Karel Krýda
2fe5c090aa small fixes 2022-05-08 20:50:08 +02:00
Karel Krýda
ed218e73bb UI improvements 2022-05-08 20:03:24 +02:00
Karel Krýda
9a35386841 Merge branch 'master' into master 2022-05-06 11:24:21 +02:00
Karel Krýda
2b14bdae62 Merge branch 'master' into master 2022-05-01 12:40:34 +02:00
Karel Krýda
31b90d12a4 Added the ability to choose on which status pages maintenance information should be displayed 2022-04-30 17:17:22 +02:00
Karel Krýda
b4ffcc5555 Added JSDoc 2022-04-30 15:50:05 +02:00
Karel Krýda
57368c8c6c More modern look of maintenance information on status page (same design as for the new incident system) 2022-04-30 15:32:56 +02:00
Karel Krýda
11ef22edec Fixed remaining lint errors 2022-04-30 15:13:13 +02:00
Karel Krýda
f78d01d770 Resolve lint errors 2022-04-30 14:57:08 +02:00
Karel Krýda
7532acc95d Resolve conflicts 2022-04-30 14:33:54 +02:00
Karel Krýda
ed84e56a85 Merge remote-tracking branch 'origin_kuma/master'
# Conflicts:
#	package-lock.json
#	server/database.js
#	server/model/monitor.js
#	server/routers/api-router.js
#	server/server.js
#	src/components/MonitorList.vue
#	src/components/PingChart.vue
#	src/icon.js
#	src/pages/DashboardHome.vue
#	src/pages/StatusPage.vue
#	src/router.js
#	src/util.js
2022-04-30 13:40:34 +02:00
Karel Krýda
b49e5d5c39 The SQL query to determine if the monitor is under maintenance is now in its own method. 2022-01-25 19:07:27 +01:00
Karel Krýda
e7b2832967 The start and end dates of the maintenance are now stored in UTC, which allows it to be converted between time zones 2022-01-24 22:33:15 +01:00
Karel Krýda
5fda1f0f59 minor fixes (missing commas, spaces, translations) 2022-01-23 20:33:39 +01:00
Karel Krýda
0d3414c6d6 A complete maintenance planning system has been created 2022-01-23 15:22:00 +01:00
145 changed files with 9300 additions and 4705 deletions

View File

@@ -31,6 +31,9 @@ tsconfig.json
/tmp
/babel.config.js
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck
### .gitignore content (commented rules are duplicated)

View File

@@ -19,3 +19,6 @@ indent_size = 2
[*.vue]
trim_trailing_whitespace = false
[*.go]
indent_style = tab

View File

@@ -12,13 +12,11 @@ jobs:
- uses: actions/stale@v5
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
days-before-stale: 90
days-before-close: 2
days-before-pr-stale: 999999999
days-before-pr-close: 1
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
exempt-issue-assignees: 'louislam'
exempt-pr-assignees: 'louislam'
operations-per-run: 200

4
.gitignore vendored
View File

@@ -16,3 +16,7 @@ dist-ssr
cypress/videos
cypress/screenshots
/extra/healthcheck.exe
/extra/healthcheck
/extra/healthcheck-armv7

View File

@@ -15,11 +15,10 @@ It is a self-hosted monitoring tool like "Uptime Robot".
Try it!
https://demo.uptime.kuma.pet
- Tokyo Demo Server: https://demo.uptime.kuma.pet (Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors))
- Europe Demo Server: https://demo.uptime-kuma.karimi.dev:27000 (Provided by [@mhkarimi1383](https://github.com/mhkarimi1383))
It is a temporary live demo, all data will be deleted after 10 minutes. The server is located in Tokyo, so if you live far from there, it may affect your experience. I suggest that you should install and try it out for the best demo experience.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
It is a temporary live demo, all data will be deleted after 10 minutes. Use the one that is closer to you, but I suggest that you should install and try it out for the best demo experience.
## ⭐ Features

28
config/cypress.config.js Normal file
View File

@@ -0,0 +1,28 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -1,33 +0,0 @@
const PuppeteerEnvironment = require("jest-environment-puppeteer");
const util = require("util");
class DebugEnv extends PuppeteerEnvironment {
async handleTestEvent(event, state) {
const ignoredEvents = [
"setup",
"add_hook",
"start_describe_definition",
"add_test",
"finish_describe_definition",
"run_start",
"run_describe_start",
"test_start",
"hook_start",
"hook_success",
"test_fn_start",
"test_fn_success",
"test_done",
"run_describe_finish",
"run_finish",
"teardown",
"test_fn_failure",
];
if (!ignoredEvents.includes(event.name)) {
console.log(
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
);
}
}
}
module.exports = DebugEnv;

View File

@@ -1,5 +0,0 @@
module.exports = {
"rootDir": "..",
"testRegex": "./test/frontend.spec.js",
};

View File

@@ -1,20 +0,0 @@
module.exports = {
"launch": {
"dumpio": true,
"slowMo": 500,
"headless": process.env.HEADLESS_TEST || false,
"userDataDir": "./data/test-chrome-profile",
args: [
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-default-browser-check",
"--no-experiments",
"--no-first-run",
"--no-pings",
"--no-sandbox",
"--no-zygote",
"--single-process",
],
}
};

View File

@@ -1,12 +0,0 @@
module.exports = {
"verbose": true,
"preset": "jest-puppeteer",
"globals": {
"__DEV__": true
},
"testRegex": "./test/e2e.spec.js",
"testEnvironment": "./config/jest-debug-env.js",
"rootDir": "..",
"testTimeout": 30000,
};

View File

@@ -1,15 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -1,24 +0,0 @@
import { actor } from "../support/actors/actor";
import { DEFAULT_USER_DATA } from "../support/const/user-data";
import { DashboardPage } from "../support/pages/dasboard-page";
import { SetupPage } from "../support/pages/setup-page";
describe("user can create a new account on setup page", () => {
before(() => {
cy.visit("/setup");
});
it("user can create new account", () => {
cy.url().should("be.equal", SetupPage.url);
actor.setupTask.fillAndSubmitSetupForm(
DEFAULT_USER_DATA.username,
DEFAULT_USER_DATA.password,
DEFAULT_USER_DATA.password
);
cy.url().should("be.equal", DashboardPage.url);
cy.get('[role="alert"]')
.should("be.visible")
.and("contain.text", "Added Successfully.");
});
});

View File

@@ -1,8 +0,0 @@
import { SetupTask } from "../tasks/setup-task";
class Actor {
setupTask: SetupTask = new SetupTask();
}
const actor = new Actor();
export { actor };

View File

@@ -1 +0,0 @@
import "./commands";

View File

@@ -1,15 +0,0 @@
import { SetupPage } from "../pages/setup-page";
export class SetupTask {
fillAndSubmitSetupForm(
username: string,
password: string,
passwordRepeat: string
) {
cy.get(SetupPage.usernameInput).type(username);
cy.get(SetupPage.passWordInput).type(password);
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
cy.get(SetupPage.submitSetupForm).click();
}
}

25
db/patch-grpc-monitor.sql Normal file
View File

@@ -0,0 +1,25 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD grpc_url VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_protobuf TEXT default null;
ALTER TABLE monitor
ADD grpc_body TEXT default null;
ALTER TABLE monitor
ADD grpc_metadata TEXT default null;
ALTER TABLE monitor
ADD grpc_method VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_service_name VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_enable_tls BOOLEAN default 0 not null;
COMMIT;

View File

@@ -0,0 +1,83 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
DROP TABLE IF EXISTS maintenance_status_page;
DROP TABLE IF EXISTS monitor_maintenance;
DROP TABLE IF EXISTS maintenance;
DROP TABLE IF EXISTS maintenance_timeslot;
-- maintenance
CREATE TABLE [maintenance] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[title] VARCHAR(150) NOT NULL,
[description] TEXT NOT NULL,
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
[active] BOOLEAN NOT NULL DEFAULT 1,
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
[start_date] DATETIME,
[end_date] DATETIME,
[start_time] TIME,
[end_time] TIME,
[weekdays] VARCHAR2(250) DEFAULT '[]',
[days_of_month] TEXT DEFAULT '[]',
[interval_day] INTEGER
);
CREATE INDEX [manual_active] ON [maintenance] (
[strategy],
[active]
);
CREATE INDEX [active] ON [maintenance] ([active]);
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
-- maintenance_status_page
CREATE TABLE maintenance_status_page (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
status_page_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX [status_page_id_index]
ON [maintenance_status_page]([status_page_id]);
CREATE INDEX [maintenance_id_index]
ON [maintenance_status_page]([maintenance_id]);
-- maintenance_timeslot
CREATE TABLE [maintenance_timeslot] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[start_date] DATETIME NOT NULL,
[end_date] DATETIME,
[generated_next] BOOLEAN DEFAULT 0
);
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
[maintenance_id] DESC,
[start_date] DESC,
[end_date] DESC
);
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
-- monitor_maintenance
CREATE TABLE monitor_maintenance (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (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 [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
COMMIT;

View File

@@ -4,5 +4,5 @@ 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==1.0.0 && \
pip3 --no-cache-dir install apprise==1.2.0 && \
rm -rf /root/.cache

View File

@@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \
apt --yes --no-install-recommends 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==1.0.0 && \
pip3 --no-cache-dir install apprise==1.2.0 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove

View File

@@ -1,30 +1,57 @@
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################
FROM golang:1.19.4-buster AS build_healthcheck
WORKDIR /app
ARG TARGETPLATFORM
COPY ./extra/ ./extra/
# Compile healthcheck.go
RUN apt update
RUN apt --yes --no-install-recommends install curl
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash
RUN apt --yes --no-install-recommends install nodejs
RUN node -v
RUN node ./extra/build-healthcheck.js $TARGETPLATFORM
############################################
# Build in Node.js
############################################
FROM louislam/uptime-kuma:base-debian AS build
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY . .
COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
RUN npm ci --production && \
chmod +x /app/extra/entrypoint.sh
############################################
# ⭐ Main Image
############################################
FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app
# 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
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
############################################
# Mark as Nightly
############################################
FROM release AS nightly
RUN npm run mark-as-nightly
############################################
# Build an image for testing pr
############################################
FROM louislam/uptime-kuma:base-debian AS pr-test
WORKDIR /app
@@ -54,8 +81,9 @@ VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["npm", "run", "start-pr-test"]
############################################
# Upload the artifact to Github
############################################
FROM louislam/uptime-kuma:base-debian AS upload-artifact
WORKDIR /
RUN apt update && \

View File

@@ -0,0 +1,27 @@
const childProcess = require("child_process");
const fs = require("fs");
const platform = process.argv[2];
if (!platform) {
console.error("No platform??");
process.exit(1);
}
if (platform === "linux/arm/v7") {
console.log("Arch: armv7");
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
console.log("Already built in the host, skip.");
process.exit(0);
} else {
console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build.");
}
} else {
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.rmSync("./extra/healthcheck-armv7");
}
}
const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
console.log(output);

77
extra/healthcheck.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"crypto/tls"
"io/ioutil"
"log"
"net/http"
"os"
"runtime"
"time"
)
func main() {
isFreeBSD := runtime.GOOS == "freebsd"
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
client := http.Client{
Timeout: 28 * time.Second,
}
sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY")
if len(sslKey) == 0 {
sslKey = os.Getenv("SSL_KEY")
}
sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT")
if len(sslCert) == 0 {
sslCert = os.Getenv("SSL_CERT")
}
hostname := os.Getenv("UPTIME_KUMA_HOST")
if len(hostname) == 0 && !isFreeBSD {
hostname = os.Getenv("HOST")
}
if len(hostname) == 0 {
hostname = "127.0.0.1"
}
port := os.Getenv("UPTIME_KUMA_PORT")
if len(port) == 0 {
port = os.Getenv("PORT")
}
if len(port) == 0 {
port = "3001"
}
protocol := ""
if len(sslKey) != 0 && len(sslCert) != 0 {
protocol = "https"
} else {
protocol = "http"
}
url := protocol + "://" + hostname + ":" + port
log.Println("Checking " + url)
resp, err := client.Get(url)
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
}

View File

@@ -1,4 +1,5 @@
/*
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const { FBSD } = require("../server/util-server");

View File

@@ -5,7 +5,7 @@ const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = oldVersion + "-nightly";
const newVersion = oldVersion + "-nightly-" + util.genSecret(8);
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);

View File

@@ -1,51 +1,45 @@
// Need to use ES6 to read language files
import fs from "fs";
import path from "path";
import util from "util";
import rmSync from "../fs-rmSync.js";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/**
* Look ma, it's cp -R.
* @param {string} src The path to the thing to copy.
* @param {string} dest The path to the new copy.
* Copy across the required language files
* Creates a local directory (./languages) and copies the required files
* into it.
* @param {string} langCode Code of language to update. A file will be
* created with this code if one does not already exist
* @param {string} baseLang The second base language file to copy. This
* will be ignored if set to "en" as en.js is copied by default
*/
const copyRecursiveSync = function (src, dest) {
let exists = fs.existsSync(src);
let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory();
function copyFiles(langCode, baseLang) {
if (fs.existsSync("./languages")) {
rmSync("./languages", { recursive: true });
}
fs.mkdirSync("./languages");
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
if (!fs.existsSync(`../../src/languages/${langCode}.js`)) {
fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a"));
} else {
fs.copyFileSync(src, dest);
fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`);
}
fs.copyFileSync("../../src/languages/en.js", "./languages/en.js");
if (baseLang !== "en") {
fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`);
}
};
console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode);
if (fs.existsSync("./languages")) {
rmSync("./languages", { recursive: true });
}
copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages");
console.log("Files:", files);
for (const file of files) {
if (! file.endsWith(".js")) {
console.log("Skipping " + file);
continue;
}
/**
* Update the specified language file
* @param {string} langCode Language code to update
* @param {string} baseLang Second language to copy keys from
*/
async function updateLanguage(langCode, baseLangCode) {
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
let file = langCode + ".js";
console.log("Processing " + file);
const lang = await import("./languages/" + file);
@@ -83,5 +77,20 @@ for (const file of files) {
fs.writeFileSync(`../../src/languages/${file}`, code);
}
// Get command line arguments
const baseLangCode = process.env.npm_config_baselang || "en";
const langCode = process.env.npm_config_language;
// We need the file to edit
if (langCode == null) {
throw new Error("Argument --language=<code> must be provided");
}
console.log("Base Lang: " + baseLangCode);
console.log("Updating: " + langCode);
copyFiles(langCode, baseLangCode);
await updateLanguage(langCode, baseLangCode);
rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint...");

6573
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.18.2",
"version": "1.19.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -23,11 +23,9 @@
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --test",
"test": "node test/prepare-test-server.js && npm run jest-backend",
"test-with-build": "npm run build && npm test",
"jest": "node test/prepare-jest.js && npm run jest-frontend && npm run jest-backend",
"jest-frontend": "cross-env TEST_FRONTEND=1 jest --config=./config/jest-frontend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --config=./config/jest-backend.config.js",
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
"tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
@@ -40,7 +38,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.18.2 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.19.0 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -53,8 +51,7 @@
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"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",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
@@ -62,52 +59,58 @@
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
"cy:run": "npx cypress run --browser chrome --headless"
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
},
"dependencies": {
"@louislam/sqlite3": "~15.0.6",
"@grpc/grpc-js": "~1.7.3",
"@louislam/sqlite3": "15.1.2",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
"axios-ntlm": "^1.3.0",
"badge-maker": "^3.3.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0",
"chardet": "~1.4.0",
"check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10",
"chroma-js": "^2.1.2",
"cheerio": "~1.0.0-rc.12",
"chroma-js": "~2.4.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "^1.7.4",
"dayjs": "^1.11.0",
"compression": "~1.7.4",
"dayjs": "~1.11.5",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "^2.1.7",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"iconv-lite": "^0.6.3",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"jsesc": "~3.0.2",
"jsonwebtoken": "~8.5.1",
"jwt-decode": "^3.1.2",
"limiter": "^2.1.0",
"mqtt": "^4.2.8",
"mssql": "^8.1.0",
"jwt-decode": "~3.1.2",
"limiter": "~2.1.0",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "^1.0.0",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"pg": "^8.7.3",
"pg-connection-string": "^2.5.0",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1",
"redbean-node": "0.1.4",
"socket.io": "~4.4.1",
"socket.io-client": "~4.4.1",
"socket.io": "~4.5.3",
"socket.io-client": "~4.5.3",
"socks-proxy-agent": "6.1.1",
"tar": "^6.1.11",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2"
},
@@ -124,33 +127,32 @@
"@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36",
"@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"core-js": "~3.26.1",
"cross-env": "~7.0.3",
"cypress": "^10.1.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "^0.3.10",
"favico.js": "~0.3.10",
"jest": "~27.2.5",
"jest-puppeteer": "~6.0.3",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4",
"prismjs": "^1.27.0",
"puppeteer": "~13.1.3",
"prismjs": "~1.29.0",
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~14.7.1",
"stylelint-config-standard": "~25.0.0",
"terser": "^5.15.0",
"terser": "~5.15.0",
"timezones-list": "~3.0.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
@@ -160,10 +162,10 @@
"vue-chart-3": "3.0.9",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.1.9",
"vue-i18n": "~9.2.2",
"vue-image-crop-upload": "~3.0.3",
"vue-multiselect": "~3.0.0-alpha.2",
"vue-prism-editor": "^2.0.0-alpha.2",
"vue-prism-editor": "~2.0.0-alpha.2",
"vue-qrcode": "~1.0.0",
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",

View File

@@ -1,6 +1,8 @@
const https = require("https");
const http = require("http");
const CacheableLookup = require("cacheable-lookup");
const { Settings } = require("./settings");
const { log } = require("../src/util");
class CacheableDnsHttpAgent {
@@ -9,12 +11,30 @@ class CacheableDnsHttpAgent {
static httpAgentList = {};
static httpsAgentList = {};
static enable = false;
/**
* Register cacheable to global agents
* Register/Disable cacheable to global agents
*/
static registerGlobalAgent() {
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
static async update() {
log.debug("CacheableDnsHttpAgent", "update");
let isEnable = await Settings.get("dnsCache");
if (isEnable !== this.enable) {
log.debug("CacheableDnsHttpAgent", "value changed");
if (isEnable) {
log.debug("CacheableDnsHttpAgent", "enable");
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
} else {
log.debug("CacheableDnsHttpAgent", "disable");
this.cacheable.uninstall(http.globalAgent);
this.cacheable.uninstall(https.globalAgent);
}
}
this.enable = isEnable;
}
static install(agent) {
@@ -26,6 +46,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agent}
*/
static getHttpsAgent(agentOptions) {
if (!this.enable) {
return new https.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpsAgentList)) {
this.httpsAgentList[key] = new https.Agent(agentOptions);
@@ -39,6 +63,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agents}
*/
static getHttpAgent(agentOptions) {
if (!this.enable) {
return new http.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpAgentList)) {
this.httpAgentList[key] = new http.Agent(agentOptions);

View File

@@ -25,7 +25,7 @@ exports.startInterval = () => {
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
exports.latestVersion = res.data.beta;
return;
}

View File

@@ -4,7 +4,8 @@
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
@@ -121,7 +122,9 @@ async function sendInfo(socket) {
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
primaryBaseURL: await setting("primaryBaseURL")
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
});
}

View File

@@ -62,8 +62,10 @@ class Database {
"patch-add-clickable-status-page-link.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-grpc-monitor.sql": true,
"patch-add-radius-monitor.sql": true,
"patch-monitor-add-resend-interval.sql": true,
"patch-maintenance-table2.sql": true,
};
/**
@@ -150,9 +152,6 @@ class Database {
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
await R.exec("PRAGMA busy_timeout = 5000");
// This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous

View File

@@ -1,8 +1,3 @@
const dayjs = require("dayjs");
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");
/**
@@ -10,6 +5,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
* 0 = DOWN
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
*/
class Heartbeat extends BeanModel {

217
server/model/maintenance.js Normal file
View File

@@ -0,0 +1,217 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
class Maintenance extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
let dateRange = [];
if (this.start_date) {
dateRange.push(utcToLocal(this.start_date));
if (this.end_date) {
dateRange.push(utcToLocal(this.end_date));
}
}
let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
timeRange.push(endTime);
let obj = {
id: this.id,
title: this.title,
description: this.description,
strategy: this.strategy,
intervalDay: this.interval_day,
active: !!this.active,
dateRange: dateRange,
timeRange: timeRange,
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [],
};
const timeslotList = await this.getTimeslotList();
for (let timeslot of timeslotList) {
obj.timeslotList.push(await timeslot.toPublicJSON());
}
if (!Array.isArray(obj.weekdays)) {
obj.weekdays = [];
}
if (!Array.isArray(obj.daysOfMonth)) {
obj.daysOfMonth = [];
}
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj;
}
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
}
/**
* Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC
* @returns {Object}
*/
async toJSON(timezone = null) {
return this.toPublicJSON(timezone);
}
getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) {
return a - b;
});
}
getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) {
return a - b;
});
}
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
getDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day
if (duration < 0) {
duration += 24 * 3600;
}
return duration;
}
static jsonToBean(bean, obj) {
if (obj.id) {
bean.id = obj.id;
}
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title;
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]);
if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]);
}
}
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
return bean;
}
/**
* SQL conditions for active maintenance
* @returns {string}
*/
static getActiveMaintenanceSQLCondition() {
return `
(
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
)
`;
}
/**
* SQL conditions for active and future maintenance
* @returns {string}
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
}
}
module.exports = Maintenance;

View File

@@ -0,0 +1,189 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel {
async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
const obj = {
id: this.id,
startDate: this.start_date,
endDate: this.end_date,
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
serverTimezoneOffset,
};
return obj;
}
async toJSON() {
return await this.toPublicJSON();
}
/**
* @param {Maintenance} maintenance
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
* @param {boolean} removeExist Remove existing timeslot before create
* @returns {Promise<MaintenanceTimeslot>}
*/
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
if (removeExist) {
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
maintenance.id
]);
}
if (maintenance.strategy === "manual") {
log.debug("maintenance", "No need to generate timeslot for manual type");
} else if (maintenance.strategy === "single") {
let bean = R.dispense("maintenance_timeslot");
bean.maintenance_id = maintenance.id;
bean.start_date = maintenance.start_date;
bean.end_date = maintenance.end_date;
bean.generated_next = true;
return await R.store(bean);
} else if (maintenance.strategy === "recurring-interval") {
// Prevent dead loop, in case interval_day is not set
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
maintenance.interval_day = 1;
}
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
return startDateTime.add(maintenance.interval_day, "day");
}, () => {
return true;
});
} else if (maintenance.strategy === "recurring-weekday") {
let dayOfWeekList = maintenance.getDayOfWeekList();
log.debug("timeslot", dayOfWeekList);
if (dayOfWeekList.length <= 0) {
log.debug("timeslot", "No weekdays selected?");
return null;
}
const isValid = (startDateTime) => {
log.debug("timeslot", "nextDateTime: " + startDateTime);
let day = startDateTime.local().day();
log.debug("timeslot", "nextDateTime.day(): " + day);
return dayOfWeekList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else if (maintenance.strategy === "recurring-day-of-month") {
let dayOfMonthList = maintenance.getDayOfMonthList();
if (dayOfMonthList.length <= 0) {
log.debug("timeslot", "No day selected?");
return null;
}
const isValid = (startDateTime) => {
let day = parseInt(startDateTime.local().format("D"));
log.debug("timeslot", "day: " + day);
// Check 1-31
if (dayOfMonthList.includes(day)) {
return startDateTime;
}
// Check "lastDay1","lastDay2"...
let daysInMonth = startDateTime.daysInMonth();
let lastDayList = [];
// Small first, e.g. 28 > 29 > 30 > 31
for (let i = 4; i >= 1; i--) {
if (dayOfMonthList.includes("lastDay" + i)) {
lastDayList.push(daysInMonth - i + 1);
}
}
log.debug("timeslot", lastDayList);
return lastDayList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else {
throw new Error("Unknown maintenance strategy");
}
}
/**
* Generate a next timeslot for all recurring types
* @param maintenance
* @param minDate
* @param {function} nextDayCallback The logic how to get the next possible day
* @param {function} isValidCallback Check the day whether is matched the current strategy
* @returns {Promise<null|MaintenanceTimeslot>}
*/
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
let bean = R.dispense("maintenance_timeslot");
let duration = maintenance.getDuration();
let startDateTime = maintenance.getStartDateTime();
let endDateTime;
// Keep generating from the first possible date, until it is ok
while (true) {
log.debug("timeslot", "startDateTime: " + startDateTime.format());
// Handling out of effective date range
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
log.debug("timeslot", "Out of effective date range");
return null;
}
endDateTime = startDateTime.add(duration, "second");
// If endDateTime is out of effective date range, use the end datetime from effective date range
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
endDateTime = dayjs.utc(maintenance.end_date);
}
// If minDate is set, the endDateTime must be bigger than it.
// And the endDateTime must be bigger current time
// Is valid under current recurring strategy
if (
(!minDate || endDateTime.diff(minDate) > 0) &&
endDateTime.diff(dayjs()) > 0 &&
isValidCallback(startDateTime)
) {
break;
}
startDateTime = nextDayCallback(startDateTime);
}
bean.maintenance_id = maintenance.id;
bean.start_date = localToUTC(startDateTime);
bean.end_date = localToUTC(endDateTime);
bean.generated_next = false;
return await R.store(bean);
}
}
module.exports = MaintenanceTimeslot;

View File

@@ -1,13 +1,9 @@
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 axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@@ -18,12 +14,15 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
const { UptimeCacheList } = require("../uptime-cache-list");
/**
* status:
* 0 = DOWN
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
*/
class Monitor extends BeanModel {
@@ -37,6 +36,7 @@ class Monitor extends BeanModel {
id: this.id,
name: this.name,
sendUrl: this.sendUrl,
maintenance: await Monitor.isUnderMaintenance(this.id),
};
if (this.sendUrl) {
@@ -90,26 +90,23 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
grpcUrl: this.grpcUrl,
grpcProtobuf: this.grpcProtobuf,
grpcMethod: this.grpcMethod,
grpcServiceName: this.grpcServiceName,
grpcEnableTls: this.getGrpcEnableTls(),
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
radiusSecret: this.radiusSecret,
};
if (includeSensitiveData) {
@@ -117,12 +114,23 @@ class Monitor extends BeanModel {
...data,
headers: this.headers,
body: this.body,
grpcBody: this.grpcBody,
grpcMetadata: this.grpcMetadata,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
};
}
data.includeSensitiveData = includeSensitiveData;
return data;
}
@@ -167,6 +175,14 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Parse to boolean
* @returns {boolean}
*/
getGrpcEnableTls() {
return Boolean(this.grpcEnableTls);
}
/**
* Get accepted status codes
* @returns {Object}
@@ -230,7 +246,10 @@ class Monitor extends BeanModel {
}
try {
if (this.type === "http" || this.type === "keyword") {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@@ -249,12 +268,16 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
// Axios Options
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
...(this.body ? { data: JSON.parse(this.body) } : {}),
timeout: this.interval * 1000 * 0.8,
headers: {
// Fix #2253
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
"Accept-Encoding": "gzip, deflate",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version,
...(this.headers ? JSON.parse(this.headers) : {}),
@@ -524,16 +547,66 @@ class Monitor extends BeanModel {
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "grpc-keyword") {
let startTime = dayjs().valueOf();
const options = {
grpcUrl: this.grpcUrl,
grpcProtobufData: this.grpcProtobuf,
grpcServiceName: this.grpcServiceName,
grpcEnableTls: this.grpcEnableTls,
grpcMethod: this.grpcMethod,
grpcBody: this.grpcBody,
keyword: this.keyword
};
const response = await grpcQuery(options);
bean.ping = dayjs().valueOf() - startTime;
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
let responseData = response.data;
if (responseData.length > 50) {
responseData = response.substring(0, 47) + "...";
}
if (response.code !== 1) {
bean.status = DOWN;
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
} else {
if (response.data.toString().includes(this.keyword)) {
bean.status = UP;
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
} else {
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
bean.status = DOWN;
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
}
}
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
// Handle monitors that were created before the
// update and as such don't have a value for
// this.port.
let port;
if (this.port == null) {
port = 1812;
} else {
port = this.port;
}
try {
const resp = await radius(
this.hostname,
@@ -541,7 +614,8 @@ class Monitor extends BeanModel {
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret
this.radiusSecret,
port
);
if (resp.code) {
bean.msg = resp.code;
@@ -594,8 +668,12 @@ class Monitor extends BeanModel {
if (isImportant) {
bean.important = true;
log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean);
if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean);
} else {
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
}
// Reset down count
bean.downCount = 0;
@@ -604,6 +682,8 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear();
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
} else {
bean.important = false;
@@ -627,11 +707,14 @@ class Monitor extends BeanModel {
beatInterval = this.retryInterval;
}
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
}
log.debug("monitor", `[${this.name}] Send to socket`);
UptimeCacheList.clearCache(this.id);
io.to(this.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, this.id, this.user_id);
@@ -816,7 +899,15 @@ class Monitor extends BeanModel {
* @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate
*/
static async calcUptime(duration, monitorID) {
static async calcUptime(duration, monitorID, forceNoCache = false) {
if (!forceNoCache) {
let cachedUptime = UptimeCacheList.getUptime(monitorID, duration);
if (cachedUptime != null) {
return cachedUptime;
}
}
const timeLogger = new TimeLogger();
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
@@ -837,7 +928,7 @@ class Monitor extends BeanModel {
-- SUM all uptime duration, also trim off the beat out of time window
SUM(
CASE
WHEN (status = 1)
WHEN (status = 1 OR status = 3)
THEN
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
@@ -875,6 +966,9 @@ class Monitor extends BeanModel {
}
}
// Cache
UptimeCacheList.addUptime(monitorID, duration, uptime);
return uptime;
}
@@ -908,11 +1002,49 @@ class Monitor extends BeanModel {
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
// MAINTENANCE -> MAINTENANCE = not important
// * MAINTENANCE -> UP = important
// * MAINTENANCE -> DOWN = important
// * DOWN -> MAINTENANCE = important
// * UP -> MAINTENANCE = important
return isFirstBeat ||
(previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
}
/**
* Is this beat important for notifications?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
* @param {const} previousBeatStatus Status of the previous beat
* @param {const} currentBeatStatus Status of the current beat
* @returns {boolean} True if is an important beat else false
*/
static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
// MAINTENANCE -> MAINTENANCE = not important
// MAINTENANCE -> UP = not important
// * MAINTENANCE -> DOWN = important
// DOWN -> MAINTENANCE = not important
// UP -> MAINTENANCE = not important
return isFirstBeat ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
return isImportant;
}
/**
@@ -1049,6 +1181,35 @@ class Monitor extends BeanModel {
monitorID
]);
}
/**
* Check if monitor is under maintenance
* @param {number} monitorID ID of monitor to check
* @returns {Promise<boolean>}
*/
static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
const maintenance = await R.getRow(`
SELECT COUNT(*) AS count
FROM monitor_maintenance mm
JOIN maintenance
ON mm.maintenance_id = maintenance.id
AND mm.monitor_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
}
validate() {
if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
}
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
}
}
module.exports = Monitor;

View File

@@ -2,6 +2,8 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const Maintenance = require("./maintenance");
class StatusPage extends BeanModel {
@@ -36,7 +38,7 @@ class StatusPage extends BeanModel {
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
const description155 = statusPage.description?.substring(0, 155) ?? "";
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
@@ -56,13 +58,19 @@ class StatusPage extends BeanModel {
head.append(`<meta property="og:description" content="${description155}" />`);
// Preload data
const json = JSON.stringify(await StatusPage.getStatusPageData(statusPage));
head.append(`
<script>
window.preloadData = ${json}
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), {
"isScriptContext": true
});
const script = $(`
<script id="preload-data" data-json="{}">
window.preloadData = ${escapedJSONObject};
</script>
`);
head.append(script);
// manifest.json
$("link[rel=manifest]").attr("href", `/api/status-page/${statusPage.slug}/manifest.json`);
@@ -83,6 +91,8 @@ class StatusPage extends BeanModel {
incident = incident.toPublicJSON();
}
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
@@ -100,7 +110,8 @@ class StatusPage extends BeanModel {
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
publicGroupList,
maintenanceList,
};
}
@@ -259,6 +270,38 @@ class StatusPage extends BeanModel {
}
}
/**
* Get list of maintenances
* @param {number} statusPageId ID of status page to get maintenance for
* @returns {Object} Object representing maintenances sanitized for public
*/
static async getMaintenanceList(statusPageId) {
try {
const publicMaintenanceList = [];
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
SELECT maintenance.*
FROM maintenance
JOIN maintenance_status_page
ON maintenance_status_page.maintenance_id = maintenance.id
AND maintenance_status_page.status_page_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
ORDER BY maintenance.end_date
`, [ statusPageId ]));
for (const bean of maintenanceBeanList) {
publicMaintenanceList.push(await bean.toPublicJSON());
}
return publicMaintenanceList;
} catch (error) {
return [];
}
}
}
module.exports = StatusPage;

View File

@@ -0,0 +1,20 @@
import { PluginFunc, ConfigType } from 'dayjs'
declare const plugin: PluginFunc
export = plugin
declare module 'dayjs' {
interface Dayjs {
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
offsetName(type?: 'short' | 'long'): string | undefined
}
interface DayjsTimezone {
(date: ConfigType, timezone?: string): Dayjs
(date: ConfigType, format: string, timezone?: string): Dayjs
guess(): string
setDefault(timezone?: string): void
}
const tz: DayjsTimezone
}

View File

@@ -0,0 +1,115 @@
/**
* Copy from node_modules/dayjs/plugin/timezone.js
* Try to fix https://github.com/louislam/uptime-kuma/issues/2318
* Source: https://github.com/iamkun/dayjs/tree/dev/src/plugin/utc
* License: MIT
*/
!function (t, e) {
// eslint-disable-next-line no-undef
typeof exports == "object" && typeof module != "undefined" ? module.exports = e() : typeof define == "function" && define.amd ? define(e) : (t = typeof globalThis != "undefined" ? globalThis : t || self).dayjs_plugin_timezone = e();
}(this, (function () {
"use strict";
let t = {
year: 0,
month: 1,
day: 2,
hour: 3,
minute: 4,
second: 5
};
let e = {};
return function (n, i, o) {
let r;
let a = function (t, n, i) {
void 0 === i && (i = {});
let o = new Date(t);
let r = function (t, n) {
void 0 === n && (n = {});
let i = n.timeZoneName || "short";
let o = t + "|" + i;
let r = e[o];
return r || (r = new Intl.DateTimeFormat("en-US", {
hour12: !1,
timeZone: t,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: i
}), e[o] = r), r;
}(n, i);
return r.formatToParts(o);
};
let u = function (e, n) {
let i = a(e, n);
let r = [];
let u = 0;
for (; u < i.length; u += 1) {
let f = i[u];
let s = f.type;
let m = f.value;
let c = t[s];
c >= 0 && (r[c] = parseInt(m, 10));
}
let d = r[3];
let l = d === 24 ? 0 : d;
let v = r[0] + "-" + r[1] + "-" + r[2] + " " + l + ":" + r[4] + ":" + r[5] + ":000";
let h = +e;
return (o.utc(v).valueOf() - (h -= h % 1e3)) / 6e4;
};
let f = i.prototype;
f.tz = function (t, e) {
void 0 === t && (t = r);
let n = this.utcOffset();
let i = this.toDate();
let a = i.toLocaleString("en-US", { timeZone: t }).replace("\u202f", " ");
let u = Math.round((i - new Date(a)) / 1e3 / 60);
let f = o(a).$set("millisecond", this.$ms).utcOffset(15 * -Math.round(i.getTimezoneOffset() / 15) - u, !0);
if (e) {
let s = f.utcOffset();
f = f.add(n - s, "minute");
}
return f.$x.$timezone = t, f;
}, f.offsetName = function (t) {
let e = this.$x.$timezone || o.tz.guess();
let n = a(this.valueOf(), e, { timeZoneName: t }).find((function (t) {
return t.type.toLowerCase() === "timezonename";
}));
return n && n.value;
};
let s = f.startOf;
f.startOf = function (t, e) {
if (!this.$x || !this.$x.$timezone) {
return s.call(this, t, e);
}
let n = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
return s.call(n, t, e).tz(this.$x.$timezone, !0);
}, o.tz = function (t, e, n) {
let i = n && e;
let a = n || e || r;
let f = u(+o(), a);
if (typeof t != "string") {
return o(t).tz(a);
}
let s = function (t, e, n) {
let i = t - 60 * e * 1e3;
let o = u(i, n);
if (e === o) {
return [ i, e ];
}
let r = u(i -= 60 * (o - e) * 1e3, n);
return o === r ? [ i, o ] : [ t - 60 * Math.min(o, r) * 1e3, Math.max(o, r) ];
}(o.utc(t, i).valueOf(), f, a);
let m = s[0];
let c = s[1];
let d = o(m).utcOffset(c);
return d.$x.$timezone = a, d;
}, o.tz.guess = function () {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}, o.tz.setDefault = function (t) {
r = t;
};
};
}));

View File

@@ -0,0 +1,24 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class FreeMobile extends NotificationProvider {
name = "FreeMobile";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
"user": notification.freemobileUser,
"pass": notification.freemobilePass,
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = FreeMobile;

View File

@@ -9,7 +9,7 @@ class Ntfy extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
let headers = {};
if (notification.ntfyusername.length > 0) {
if (notification.ntfyusername) {
headers = {
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
};
@@ -20,6 +20,11 @@ class Ntfy extends NotificationProvider {
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
};
if (notification.ntfyIcon) {
data.icon = notification.ntfyIcon;
}
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
return okMsg;

View File

@@ -19,26 +19,26 @@ class Pushbullet extends NotificationProvider {
}
};
if (heartbeatJSON == null) {
let testdata = {
let data = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
"body": msg,
};
await axios.post(pushbulletUrl, testdata, config);
await axios.post(pushbulletUrl, data, config);
} else if (heartbeatJSON["status"] === DOWN) {
let downdata = {
let downData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
};
await axios.post(pushbulletUrl, downdata, config);
await axios.post(pushbulletUrl, downData, config);
} else if (heartbeatJSON["status"] === UP) {
let updata = {
let upData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
};
await axios.post(pushbulletUrl, updata, config);
await axios.post(pushbulletUrl, upData, config);
}
return okMsg;
} catch (error) {

View File

@@ -0,0 +1,71 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSEagle extends NotificationProvider {
name = "SMSEagle";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
}
};
let postData;
let sendMethod;
let recipientType;
let encoding = (notification.smseagleEncoding) ? "1" : "0";
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "sms.send_tocontact";
}
if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "sms.send_togroup";
}
if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "sms.send_sms";
}
let params = {
access_token: notification.smseagleToken,
[recipientType]: notification.smseagleRecipient,
message: msg,
responsetype: "extended",
unicode: encoding,
highpriority: priority
};
postData = {
method: sendMethod,
params: params
};
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
let error = "";
if (resp.data.result && resp.data.result.error_text) {
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
} else {
error = "SMSEagle API returned an unexpected response";
}
throw new Error(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSEagle;

View File

@@ -16,20 +16,29 @@ class Webhook extends NotificationProvider {
msg,
};
let finalData;
let config = {};
let config = {
headers: {}
};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
};
config.headers = finalData.getHeaders();
} else {
finalData = data;
}
if (notification.webhookAdditionalHeaders) {
try {
config.headers = {
...config.headers,
...JSON.parse(notification.webhookAdditionalHeaders)
};
} catch (err) {
throw "Additional Headers is not a valid JSON";
}
}
await axios.post(notification.webhookURL, finalData, config);
return okMsg;

View File

@@ -9,6 +9,7 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
@@ -31,6 +32,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMSEagle = require("./notification-providers/smseagle");
const SMTP = require("./notification-providers/smtp");
const Squadcast = require("./notification-providers/squadcast");
const Stackfield = require("./notification-providers/stackfield");
@@ -63,6 +65,7 @@ class Notification {
new DingDing(),
new Discord(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),
new Gorush(),
new Gotify(),
@@ -87,6 +90,7 @@ class Notification {
new Signal(),
new SMSManager(),
new Slack(),
new SMSEagle(),
new SMTP(),
new Squadcast(),
new Stackfield(),

View File

@@ -105,7 +105,7 @@ Ping.prototype.send = function (callback) {
let _exited;
let _errored;
this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
this._ping.on("error", function (err) { // handle binary errors
_errored = true;

View File

@@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy {
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks5h", "socks4" ];
/**
* Saves and updates given proxy entity
@@ -126,6 +126,7 @@ class Proxy {
break;
case "socks":
case "socks5":
case "socks5h":
case "socks4":
agent = new SocksProxyAgent({
...httpAgentOptions,

View File

@@ -4,7 +4,7 @@ const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, DOWN, flipStatus, log } = require("../../src/util");
const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { makeBadge } = require("badge-maker");
@@ -67,6 +67,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
if (await Monitor.isUnderMaintenance(monitor.id)) {
msg = "Monitor under maintenance";
status = MAINTENANCE;
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);
@@ -87,7 +92,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
ok: true,
});
if (bean.important) {
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
await Monitor.sendNotification(isFirstBeat, monitor, bean);
}

View File

@@ -5,6 +5,12 @@
*/
console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
const dayjs = require("dayjs");
dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("./modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14;
@@ -33,6 +39,7 @@ log.info("server", "Importing Node libraries");
const fs = require("fs");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
@@ -127,6 +134,10 @@ const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
app.use(express.json());
@@ -154,8 +165,9 @@ let needSetup = false;
(async () => {
Database.init(args);
await initDatabase(testMode);
await server.initAfterDatabaseReady();
exports.entryPage = await setting("entryPage");
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
log.info("server", "Adding route");
@@ -176,14 +188,15 @@ let needSetup = false;
log.debug("entry", `Request Domain: ${hostname}`);
const uptimeKumaEntryPage = server.entryPage;
if (hostname in StatusPage.domainMappingList) {
log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith("statusPage-")) {
response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
@@ -192,6 +205,7 @@ let needSetup = false;
if (isDev) {
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
response.send("OK");
});
@@ -200,7 +214,7 @@ let needSetup = false;
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (! await setting("searchEngineIndex")) {
if (!await setting("searchEngineIndex")) {
txt += " /";
}
response.setHeader("Content-Type", "text/plain");
@@ -620,6 +634,9 @@ let needSetup = false;
bean.import(monitor);
bean.user_id = socket.userID;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, notificationIDList);
@@ -695,12 +712,20 @@ let needSetup = false;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
bean.grpcUrl = monitor.grpcUrl;
bean.grpcProtobuf = monitor.grpcProtobuf;
bean.grpcMethod = monitor.grpcMethod;
bean.grpcBody = monitor.grpcBody;
bean.grpcMetadata = monitor.grpcMetadata;
bean.grpcEnableTls = monitor.grpcEnableTls;
bean.radiusUsername = monitor.radiusUsername;
bean.radiusPassword = monitor.radiusPassword;
bean.radiusCalledStationId = monitor.radiusCalledStationId;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, monitor.notificationIDList);
@@ -1055,10 +1080,15 @@ let needSetup = false;
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket);
const data = await getSettings("general");
if (!data.serverTimezone) {
data.serverTimezone = await server.getTimezone();
}
callback({
ok: true,
data: await getSettings("general"),
data: data,
});
} catch (e) {
@@ -1084,7 +1114,14 @@ let needSetup = false;
}
await setSettings("general", data);
exports.entryPage = data.entryPage;
server.entryPage = data.entryPage;
await CacheableDnsHttpAgent.update();
// Also need to apply timezone globally
if (data.serverTimezone) {
await server.setTimezone(data.serverTimezone);
}
callback({
ok: true,
@@ -1092,6 +1129,7 @@ let needSetup = false;
});
sendInfo(socket);
server.sendMaintenanceList(socket);
} catch (e) {
callback({
@@ -1450,6 +1488,8 @@ let needSetup = false;
databaseSocketHandler(socket);
proxySocketHandler(socket);
dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
generalSocketHandler(socket, server);
log.debug("server", "added all socket handlers");
@@ -1552,6 +1592,7 @@ async function afterLogin(socket, user) {
socket.join(user.id);
let monitorList = await server.sendMonitorList(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket);
sendProxyList(socket);
sendDockerHostList(socket);
@@ -1571,6 +1612,13 @@ async function afterLogin(socket, user) {
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only
if (! await Settings.get("initServerTimezone")) {
log.debug("server", "emit initServerTimezone");
socket.emit("initServerTimezone");
}
}
/**
@@ -1697,6 +1745,8 @@ async function shutdownFunction(signal) {
log.info("server", "Shutdown requested");
log.info("server", "Called signal: " + signal);
await server.stop();
log.info("server", "Stopping all monitors");
for (let id in server.monitorList) {
let monitor = server.monitorList[id];
@@ -1707,6 +1757,7 @@ async function shutdownFunction(signal) {
stopBackgroundJobs();
await cloudflaredStop();
Settings.stopCacheCleaner();
}
/** Final function called before application exits */

View File

@@ -158,6 +158,13 @@ class Settings {
delete Settings.cacheList[key];
}
}
static stopCacheCleaner() {
if (Settings.cacheCleaner) {
clearInterval(Settings.cacheCleaner);
Settings.cacheCleaner = null;
}
}
}
module.exports = {

View File

@@ -1,6 +1,7 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@@ -107,7 +108,7 @@ module.exports.autoStart = async (token) => {
/** Stop cloudflared */
module.exports.stop = async () => {
console.log("Stop cloudflared");
log.info("cloudflared", "Stop cloudflared");
if (cloudflared) {
cloudflared.stop();
}

View File

@@ -0,0 +1,20 @@
const { log } = require("../../src/util");
const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
module.exports.generalSocketHandler = (socket, server) => {
socket.on("initServerTimezone", async (timezone) => {
try {
checkLogin(socket);
log.debug("generalSocketHandler", "Timezone: " + timezone);
await Settings.set("initServerTimezone", true);
await server.setTimezone(timezone);
await sendInfo(socket);
} catch (e) {
log.warn("initServerTimezone", e.message);
}
});
};

View File

@@ -0,0 +1,311 @@
const { checkLogin } = require("../util-server");
const { log } = require("../../src/util");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const Maintenance = require("../model/maintenance");
const server = UptimeKumaServer.getInstance();
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
/**
* Handlers for Maintenance
* @param {Socket} socket Socket.io instance
*/
module.exports.maintenanceSocketHandler = (socket) => {
// Add a new maintenance
socket.on("addMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", maintenance);
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
bean.user_id = socket.userID;
let maintenanceID = await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Added Successfully.",
maintenanceID,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Edit a maintenance
socket.on("editMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
if (bean.user_id !== socket.userID) {
throw new Error("Permission denied.");
}
Maintenance.jsonToBean(bean, maintenance);
await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Saved.",
maintenanceID: bean.id,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const monitor of monitors) {
let bean = R.dispense("monitor_maintenance");
bean.import({
monitor_id: monitor.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const statusPage of statusPages) {
let bean = R.dispense("maintenance_status_page");
bean.import({
status_page_id: statusPage.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
callback({
ok: true,
maintenance: await bean.toJSON(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceList", async (callback) => {
try {
checkLogin(socket);
await server.sendMaintenanceList(socket);
callback({
ok: true,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
monitors,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
statusPages,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
if (maintenanceID in server.maintenanceList) {
delete server.maintenanceList[maintenanceID];
}
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
callback({
ok: true,
msg: "Deleted Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("pauseMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
maintenanceID,
]);
callback({
ok: true,
msg: "Paused Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("resumeMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
maintenanceID,
]);
callback({
ok: true,
msg: "Resume Successfully",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
};

View File

@@ -0,0 +1,39 @@
const { log } = require("../src/util");
class UptimeCacheList {
/**
* list[monitorID][duration]
*/
static list = {};
/**
*
* @param monitorID
* @param duration
* @return number
*/
static getUptime(monitorID, duration) {
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
return UptimeCacheList.list[monitorID][duration];
} else {
return null;
}
}
static addUptime(monitorID, duration, uptime) {
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
if (!UptimeCacheList.list[monitorID]) {
UptimeCacheList.list[monitorID] = {};
}
UptimeCacheList.list[monitorID][duration] = uptime;
}
static clearCache(monitorID) {
log.debug("UptimeCacheList", "clearCache: " + monitorID);
delete UptimeCacheList.list[monitorID];
}
}
module.exports = {
UptimeCacheList,
};

View File

@@ -9,6 +9,8 @@ const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -26,6 +28,13 @@ class UptimeKumaServer {
* @type {{}}
*/
monitorList = {};
/**
* Main maintenance list
* @type {{}}
*/
maintenanceList = {};
entryPage = "dashboard";
app = undefined;
httpServer = undefined;
@@ -37,6 +46,8 @@ class UptimeKumaServer {
*/
indexHTML = "";
generateMaintenanceTimeslotsInterval = undefined;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -72,11 +83,21 @@ class UptimeKumaServer {
}
}
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer);
}
async initAfterDatabaseReady() {
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
dayjs.tz.setDefault(process.env.TZ);
log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.generateMaintenanceTimeslots();
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
}
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
@@ -104,6 +125,40 @@ class UptimeKumaServer {
return result;
}
/**
* Send maintenance list to client
* @param {Socket} socket Socket.io instance to send to
* @returns {Object}
*/
async sendMaintenanceList(socket) {
return await this.sendMaintenanceListByUserID(socket.userID);
}
async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list);
return list;
}
/**
* Get a list of maintenances for the given user.
* @param {string} userID - The ID of the user to get maintenances for.
* @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
*/
async getMaintenanceJSONList(userID) {
let result = {};
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
userID,
]);
for (let maintenance of maintenanceList) {
result[maintenance.id] = await maintenance.toJSON();
}
return result;
}
/**
* Write error to log file
* @param {any} error The error to write
@@ -138,15 +193,58 @@ class UptimeKumaServer {
}
if (await Settings.get("trustProxy")) {
return socket.client.conn.request.headers["x-forwarded-for"]
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
} else {
return clientIP.replace(/^.*:/, "");
}
}
async getTimezone() {
let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone;
} else if (process.env.TZ) {
return process.env.TZ;
} else {
return dayjs.tz.guess();
}
}
getTimezoneOffset() {
return dayjs().format("Z");
}
async setTimezone(timezone) {
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
}
async generateMaintenanceTimeslots() {
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
for (let maintenanceTimeslot of list) {
let maintenance = await maintenanceTimeslot.maintenance;
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
maintenanceTimeslot.generated_next = true;
await R.store(maintenanceTimeslot);
}
}
async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
}
}
module.exports = {
UptimeKumaServer
};
// Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");

View File

@@ -13,14 +13,18 @@ const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const {
dictionaries: {
rfc2865: { file, attributes },
},
} = require("node-radius-utils");
const dayjs = require("dayjs");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
@@ -291,6 +295,39 @@ exports.postgresQuery = function (connectionString, query) {
});
};
/**
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mysqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
connection.promise().query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
.finally(() => {
connection.end();
});
});
};
/**
* Query radius server
* @param {string} hostname Hostname of radius server
* @param {string} username Username to use
* @param {string} password Password to use
* @param {string} calledStationId ID of called station
* @param {string} callingStationId ID of calling station
* @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on
* @returns {Promise<any>}
*/
exports.radius = function (
hostname,
username,
@@ -298,9 +335,11 @@ exports.radius = function (
calledStationId,
callingStationId,
secret,
port = 1812,
) {
const client = new radiusClient({
host: hostname,
hostPort: port,
dictionaries: [ file ],
});
@@ -431,6 +470,10 @@ const parseCertificateInfo = function (info) {
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
if (!res.request.res.socket) {
throw new Error("No socket found");
}
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
@@ -557,7 +600,7 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
exports.startUnitTest = async () => {
console.log("Starting unit test...");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const child = childProcess.spawn(npm, [ "run", "jest" ]);
const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
child.stdout.on("data", (data) => {
console.log(data.toString());
@@ -645,3 +688,112 @@ module.exports.send403 = (res, msg = "") => {
"msg": msg,
});
};
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
let offsetString;
if (timezone) {
offsetString = dayjs().tz(timezone).format("Z");
} else {
offsetString = dayjs().format("Z");
}
let hours = parseInt(offsetString.substring(1, 3));
let minutes = parseInt(offsetString.substring(4, 6));
if (
(timeObjectToUTC && offsetString.startsWith("+")) ||
(!timeObjectToUTC && offsetString.startsWith("-"))
) {
hours *= -1;
minutes *= -1;
}
obj.hours += hours;
obj.minutes += minutes;
// Handle out of bound
if (obj.minutes < 0) {
obj.minutes += 60;
obj.hours--;
} else if (obj.minutes > 60) {
obj.minutes -= 60;
obj.hours++;
}
if (obj.hours < 0) {
obj.hours += 24;
} else if (obj.hours > 24) {
obj.hours -= 24;
}
return obj;
}
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, true);
};
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, false);
};
/**
* Create gRPC client stib
* @param {Object} options from gRPC client
*/
module.exports.grpcQuery = async (options) => {
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
const protocObject = protojs.parse(grpcProtobufData);
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
const Client = grpc.makeGenericClientConstructor({});
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
const client = new Client(
grpcUrl,
credentials
);
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
const fullServiceName = method.fullName;
const serviceFQDN = fullServiceName.split(".");
const serviceMethod = serviceFQDN.pop();
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
client.makeUnaryRequest(
serviceMethodClientImpl,
arg => arg,
arg => arg,
requestData,
cb);
}, false, false);
return new Promise((resolve, _) => {
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
const responseData = JSON.stringify(response);
if (err) {
return resolve({
code: err.code,
errorMessage: err.details,
data: ""
});
} else {
log.debug("monitor:", `gRPC response: ${response}`);
return resolve({
code: 1,
errorMessage: "",
data: responseData
});
}
});
});
};

View File

@@ -22,6 +22,19 @@ textarea.form-control {
width: 10px;
}
.bg-maintenance {
color: white !important;
background-color: $maintenance !important;
}
.bg-dark {
color: white;
}
.text-maintenance {
color: $maintenance !important;
}
.list-group {
border-radius: 0.75rem;
@@ -107,6 +120,19 @@ optgroup {
}
}
.btn-normal {
$bg-color: #F5F5F5;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning {
color: white;
@@ -256,6 +282,20 @@ optgroup {
color: white;
}
.btn-normal {
$bg-color: $dark-header-bg;
color: $dark-font-color;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning {
color: $dark-font-color2;
@@ -323,6 +363,7 @@ optgroup {
&.bg-info,
&.bg-warning,
&.bg-danger,
&.bg-maintenance,
&.bg-light {
color: $dark-font-color2;
}

View File

@@ -1,6 +1,7 @@
$primary: #5cdd8b;
$danger: #dc3545;
$warning: #f8a306;
$maintenance: #1747f5;
$link-color: #111;
$border-radius: 50rem;

View File

@@ -0,0 +1,39 @@
@import "@vuepic/vue-datepicker/dist/main.css";
@import "vars.scss";
// Must use #{ }
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
.dp__theme_dark {
--dp-background-color: #{$dark-bg2};
--dp-text-color: #{$dark-font-color};
--dp-hover-color: #484848;
--dp-hover-text-color: #ffffff;
--dp-hover-icon-color: #959595;
--dp-primary-color: #{#5cdd8b};
--dp-primary-text-color: #ffffff;
--dp-secondary-color: #494949;
--dp-border-color: #{$dark-border-color};
--dp-menu-border-color: #2d2d2d;
--dp-border-color-hover: #{$dark-border-color};
--dp-disabled-color: #212121;
--dp-scroll-bar-background: #212121;
--dp-scroll-bar-color: #484848;
--dp-success-color: #{$primary};
--dp-success-color-disabled: #428f59;
--dp-icon-color: #959595;
--dp-danger-color: #e53935;
--dp-highlight-color: rgba(0, 92, 178, 0.2);
}
.dp__input {
border-radius: $border-radius;
}
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
.dp__main > div[aria-label="Datepicker input"] {
width: 100%;
}
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
margin-top: 20px;
}

View File

@@ -3,14 +3,6 @@
</template>
<script>
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export default {
props: {
/** Value of date time */

View File

@@ -5,7 +5,7 @@
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
:style="beatStyle"
:title="getBeatTitle(beat)"
/>
@@ -211,6 +211,10 @@ export default {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;

View File

@@ -42,7 +42,7 @@ export default {
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
default: "new-password",
},
/** Is the input required? */
required: {

View File

@@ -0,0 +1,44 @@
<template>
<div>
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
{{ $t("Manual") }}
</div>
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
{{ maintenance.timeslotList[0].startDateServerTimezone }}
<span class="to">-</span>
{{ maintenance.timeslotList[0].endDateServerTimezone }}
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
</div>
</div>
</template>
<script>
export default {
props: {
maintenance: {
type: Object,
required: true
},
},
};
</script>
<style lang="scss">
.timeslot {
margin-top: 5px;
display: inline-block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
.to {
margin: 0 6px;
}
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
</style>

View File

@@ -206,6 +206,16 @@ export default {
.search-icon {
padding: 10px;
color: #c0c0c0;
// Clear filter button (X)
svg[data-icon="times"] {
cursor: pointer;
transition: all ease-in-out 0.1s;
&:hover {
opacity: 0.5;
}
}
}
.search-input {

View File

@@ -16,18 +16,14 @@
</div>
</template>
<script lang="ts">
<script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification";
import { DOWN, log } from "../util.ts";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
dayjs.extend(utc);
dayjs.extend(timezone);
const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
@@ -163,7 +159,8 @@ export default {
},
chartData() {
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
let colorData = []; // Color Data for Bar Chart
let heartbeatList = this.heartbeatList ||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
@@ -185,8 +182,9 @@ export default {
});
downData.push({
x,
y: beat.status === DOWN ? 1 : 0,
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
});
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
});
return {
@@ -205,7 +203,7 @@ export default {
type: "bar",
data: downData,
borderColor: "#00000000",
backgroundColor: "#DC354568",
backgroundColor: colorData,
yAxisID: "y1",
barThickness: "flex",
barPercentage: 1,

View File

@@ -17,6 +17,7 @@
<option value="http">HTTP</option>
<option value="socks">SOCKS</option>
<option value="socks5">SOCKS v5</option>
<option value="socks5h">SOCKS v5 (+DNS)</option>
<option value="socks4">SOCKS v4</option>
</select>
</div>

View File

@@ -225,4 +225,8 @@ export default {
}
}
.bg-maintenance {
background-color: $maintenance;
}
</style>

View File

@@ -26,6 +26,10 @@ export default {
return "warning";
}
if (this.status === 3) {
return "maintenance";
}
return "secondary";
},
@@ -42,6 +46,10 @@ export default {
return this.$t("Pending");
}
if (this.status === 3) {
return this.$t("statusMaintenance");
}
return this.$t("Unknown");
},
},

View File

@@ -25,6 +25,10 @@ export default {
computed: {
uptime() {
if (this.type === "maintenance") {
return this.$t("statusMaintenance");
}
let key = this.monitor.id + "_" + this.type;
if (this.$root.uptimeList[key] !== undefined) {
@@ -35,6 +39,10 @@ export default {
},
color() {
if (this.type === "maintenance" || this.monitor.maintenance) {
return "maintenance";
}
if (this.lastHeartBeat.status === 0) {
return "danger";
}

View File

@@ -6,7 +6,7 @@
</i18n-t>
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<div class="form-text">

View File

@@ -0,0 +1,12 @@
<template>
<div class="mb-3">
<label for="freemobileUser" class="form-label">{{ $t("Free Mobile User Identifier") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobileUser" v-model="$parent.notification.freemobileUser" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="freemobilePass" class="form-label">{{ $t("Free Mobile API Key") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="freemobilePass" v-model="$parent.notification.freemobilePass" type="text" class="form-control" required>
</div>
</template>

View File

@@ -11,7 +11,7 @@
<div class="mb-3">
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="new-password" :required="true"></HiddenInput>
<div class="form-text">
{{ $t("goAlertIntegrationKeyInfo") }}

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>

View File

@@ -18,7 +18,7 @@
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
<div class="form-text">
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
<p>{{ $t('A list of Notification Services can be found in Home Assistant under "Developer Tools > Services" search for "notification" to find your device/phone name.') }}</p>
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
<p>
{{ $t("Trigger type:") }} <code>Event</code><br />

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
<b>{{ $t("Basic Settings") }}</b>

View File

@@ -9,7 +9,7 @@
</div>
<div class="mb-3">
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="new-password" :maxlength="500"></HiddenInput>
</div>
<div class="form-text">

View File

@@ -18,7 +18,7 @@
<div class="mb-3">
<label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
<div class="input-group mb-3">
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control" required>
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
</div>
</div>
<div class="mb-3">
@@ -27,6 +27,10 @@
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
</div>
<div class="mb-3">
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
<input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control">
</div>
</template>
<script>

View File

@@ -11,7 +11,7 @@
</div>
<div class="mb-3">
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div>

View File

@@ -3,7 +3,7 @@
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@@ -1,9 +1,9 @@
<template>
<div class="mb-3">
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>

View File

@@ -1,13 +1,13 @@
<template>
<div class="mb-3">
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="smseagle-url" class="form-label">{{ $t("smseagleUrl") }}</label>
<input id="smseagle-url" v-model="$parent.notification.smseagleUrl" type="text" minlength="7" class="form-control" placeholder="http://127.0.0.1" required>
</div>
<div class="mb-3">
<label for="smseagle-token" class="form-label">{{ $t("smseagleToken") }}</label>
<HiddenInput id="smseagle-token" v-model="$parent.notification.smseagleToken" :required="true"></HiddenInput>
</div>
<div class="mb-3">
<label for="smseagle-recipient-type" class="form-label">{{ $t("smseagleRecipientType") }}</label>
<select id="smseagle-recipient-type" v-model="$parent.notification.smseagleRecipientType" class="form-select">
<option value="smseagle-to" selected>{{ $t("smseagleTo") }}</option>
<option value="smseagle-group">{{ $t("smseagleGroup") }}</option>
<option value="smseagle-contact">{{ $t("smseagleContact") }}</option>
</select>
</div>
<div class="mb-3">
<label for="smseagle-recipient" class="form-label">{{ $t("smseagleRecipient") }}</label>
<input id="smseagle-recipient" v-model="$parent.notification.smseagleRecipient" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="smseagle-priority" class="form-label">{{ $t("smseaglePriority") }}</label>
<input id="smseagle-priority" v-model="$parent.notification.smseaglePriority" type="number" class="form-control" min="0" max="9" step="1" placeholder="0">
</div>
<div class="mb-3 form-check form-switch">
<label for="smseagle-encoding" class="form-label">{{ $t("smseagleEncoding") }}</label>
<input id="smseagle-encoding" v-model="$parent.notification.smseagleEncoding" type="checkbox" class="form-check-input">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@@ -1,8 +1,8 @@
<template>
<div class="mb-3">
<label for="smsmanager-key" class="form-label">API Key</label>
<label for="smsmanager-key" class="form-label">{{ $t("API Key") }}</label>
<div class="form-text">
{{ $t("SMSManager API Docs ") }}
{{ $t("SMSManager API Docs") }}
<a href="https://smsmanager.cz/api/http#send" target="_blank">{{ $t("here") }}</a>
</div>
<input id="smsmanager-key" v-model="$parent.notification.smsmanagerApiKey" type="text" class="form-control">
@@ -17,9 +17,9 @@
<div class="mb-3">
<label for="smsmanager-messageType" class="form-label">{{ $t("Gateway Type") }}</label>
<select id="smsmanager-messageType" v-model="$parent.notification.messageType" class="form-select">
<option value="economy">Economy</option>
<option value="lowcost">Lowcost</option>
<option value="high" selected>High</option>
<option value="economy">{{ $t("Economy") }}</option>
<option value="lowcost">{{ $t("Lowcost") }}</option>
<option value="high" selected>{{ $t("High") }}</option>
</select>
</div>
<div class="mb-3">

View File

@@ -34,7 +34,7 @@
<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>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="serverchan-sendkey" class="form-label">{{ $t("SendKey") }}</label>
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="serverchan-sendkey" v-model="$parent.notification.serverChanSendKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
</template>

View File

@@ -5,7 +5,7 @@
</div>
<div class="mb-3">
<label for="serwersms-key" class="form-label">{{ $t('serwersmsAPIPassword') }}</label>
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="serwersms-key" v-model="$parent.notification.serwersmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="serwersms-phone-number" class="form-label">{{ $t("serwersmsPhoneNumber") }}</label>

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="push-api-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
<a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
</i18n-t>

View File

@@ -1,22 +1,32 @@
<template>
<div class="mb-3">
<label for="webhook-url" class="form-label">{{ $t("Post URL") }}</label>
<input id="webhook-url" v-model="$parent.notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
<input
id="webhook-url"
v-model="$parent.notification.webhookURL"
type="url"
pattern="https?://.+"
class="form-control"
required
/>
</div>
<div class="mb-3">
<label for="webhook-content-type" class="form-label">{{ $t("Content Type") }}</label>
<select id="webhook-content-type" v-model="$parent.notification.webhookContentType" class="form-select" required>
<option value="json">
application/json
</option>
<option value="form-data">
multipart/form-data
</option>
<label for="webhook-content-type" class="form-label">{{
$t("Content Type")
}}</label>
<select
id="webhook-content-type"
v-model="$parent.notification.webhookContentType"
class="form-select"
required
>
<option value="json">application/json</option>
<option value="form-data">multipart/form-data</option>
</select>
<div class="form-text">
<p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
<i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>"multipart/form-data"</template>
<template #decodeFunction>
@@ -25,4 +35,44 @@
</i18n-t>
</div>
</div>
<div class="mb-3">
<i18n-t
tag="label"
class="form-label"
for="additionalHeaders"
keypath="webhookAdditionalHeadersTitle"
>
</i18n-t>
<textarea
id="additionalHeaders"
v-model="$parent.notification.webhookAdditionalHeaders"
class="form-control"
:placeholder="headersPlaceholder"
></textarea>
<div class="form-text">
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</div>
</div>
</template>
<script>
export default {
computed: {
headersPlaceholder() {
return this.$t("Example:", [
`
{
"HeaderName": "HeaderValue"
}`,
]);
},
},
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 200px;
}
</style>

View File

@@ -7,6 +7,7 @@ import ClickSendSMS from "./ClickSendSMS.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue";
@@ -32,6 +33,7 @@ import Signal from "./Signal.vue";
import SMSManager from "./SMSManager.vue";
import Slack from "./Slack.vue";
import Squadcast from "./Squadcast.vue";
import SMSEagle from "./SMSEagle.vue";
import Stackfield from "./Stackfield.vue";
import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
@@ -56,6 +58,7 @@ const NotificationFormList = {
"DingDing": DingDing,
"discord": Discord,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,
"gorush": Gorush,
"gotify": Gotify,
@@ -81,6 +84,7 @@ const NotificationFormList = {
"SMSManager": SMSManager,
"slack": Slack,
"squadcast": Squadcast,
"SMSEagle": SMSEagle,
"smtp": STMP,
"stackfield": Stackfield,
"teams": Teams,

View File

@@ -1,10 +1,10 @@
<template>
<div>
<form class="my-4" @submit.prevent="saveGeneral">
<!-- Timezone -->
<form class="my-4" autocomplete="off" @submit.prevent="saveGeneral">
<!-- Client side Timezone -->
<div class="mb-4">
<label for="timezone" class="form-label">
{{ $t("Timezone") }}
{{ $t("Display Timezone") }}
</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select">
<option value="auto">
@@ -20,6 +20,23 @@
</select>
</div>
<!-- Server Timezone -->
<div class="mb-4">
<label for="timezone" class="form-label">
{{ $t("Server Timezone") }}
</label>
<select id="timezone" v-model="settings.serverTimezone" class="form-select">
<option value="UTC">UTC</option>
<option
v-for="(timezone, index) in timezoneList"
:key="index"
:value="timezone.value"
>
{{ timezone.name }}
</option>
</select>
</div>
<!-- Search Engine -->
<div class="mb-4">
<label class="form-label">
@@ -32,7 +49,7 @@
v-model="settings.searchEngineIndex"
class="form-check-input"
type="radio"
name="flexRadioDefault"
name="searchEngineIndex"
:value="true"
required
/>
@@ -46,7 +63,7 @@
v-model="settings.searchEngineIndex"
class="form-check-input"
type="radio"
name="flexRadioDefault"
name="searchEngineIndex"
:value="false"
required
/>
@@ -105,6 +122,7 @@
name="primaryBaseURL"
placeholder="https://"
pattern="https?://.+"
autocomplete="new-password"
/>
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL">
{{ $t("Auto Get") }}
@@ -122,7 +140,7 @@
<HiddenInput
id="steamAPIKey"
v-model="settings.steamAPIKey"
autocomplete="one-time-code"
autocomplete="new-password"
/>
<div class="form-text">
{{ $t("steamApiKeyDescription") }}
@@ -132,6 +150,46 @@
</div>
</div>
<!-- DNS Cache -->
<div class="mb-4">
<label class="form-label">
{{ $t("Enable DNS Cache") }}
<div class="form-text">
{{ $t("dnsCacheDescription") }}
</div>
</label>
<div class="form-check">
<input
id="dnsCacheEnable"
v-model="settings.dnsCache"
class="form-check-input"
type="radio"
name="dnsCache"
:value="true"
required
/>
<label class="form-check-label" for="dnsCacheEnable">
{{ $t("Enable") }}
</label>
</div>
<div class="form-check">
<input
id="dnsCacheDisable"
v-model="settings.dnsCache"
class="form-check-input"
type="radio"
name="dnsCache"
:value="false"
required
/>
<label class="form-check-label" for="dnsCacheDisable">
{{ $t("Disable") }}
</label>
</div>
</div>
<!-- Save Button -->
<div>
<button class="btn btn-primary" type="submit">
@@ -145,11 +203,7 @@
<script>
import HiddenInput from "../../components/HiddenInput.vue";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { timezoneList } from "../../util-frontend";
dayjs.extend(utc);
dayjs.extend(timezone);
export default {
components: {

View File

@@ -41,7 +41,7 @@
<HiddenInput
id="cloudflareTunnelToken"
v-model="cloudflareTunnelToken"
autocomplete="one-time-code"
autocomplete="new-password"
:readonly="running"
/>
<div class="form-text">

View File

@@ -1,4 +1,4 @@
import { createI18n } from "vue-i18n/index";
import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
import en from "./languages/en";
const languageList = {
@@ -6,6 +6,7 @@ const languageList = {
"zh-HK": "繁體中文 (香港)",
"bg-BG": "Български",
"de-DE": "Deutsch (Deutschland)",
"de-CH": "Deutsch (Schweiz)",
"nl-NL": "Nederlands",
"nb-NO": "Norsk",
"es-ES": "Español",
@@ -34,6 +35,7 @@ const languageList = {
"zh-TW": "繁體中文 (台灣)",
"uk-UA": "Український",
"th-TH": "ไทย",
"el-GR": "Ελληνικά",
};
let messages = {

View File

@@ -41,6 +41,9 @@ import {
faUndo,
faPlusCircle,
faAngleDown,
faWrench,
faHeartbeat,
faFilter,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@@ -82,6 +85,9 @@ library.add(
faPlusCircle,
faAngleDown,
faLink,
faWrench,
faHeartbeat,
faFilter,
);
export { FontAwesomeIcon };

View File

@@ -1,8 +1,12 @@
# 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. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
2. Run `npm install`
3. Run `npm run update-language-files --language=<code>` where `<code>`
is a valid ISO language code:
http://www.lingoes.net/en/translator/langcode.htm. 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. Add it into `languageList` constant.
6. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.

View File

@@ -2,8 +2,8 @@ export default {
languageName: "Български",
checkEverySecond: "Ще се извършва на всеки {0} секунди",
retryCheckEverySecond: "Ще се извършва на всеки {0} секунди",
retriesDescription: "Максимакен брой опити преди маркиране на услугата като недостъпна и изпращане на известие",
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
retriesDescription: "Максимален брой опити преди маркиране на услугата като недостъпна и изпращане на известие",
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уеб сайтове",
upsideDownModeDescription: "Обръща статуса от достъпен на недостъпен. Ако услугата е достъпна, ще се вижда като НЕДОСТЪПНА.",
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
acceptedStatusCodesDescription: "Изберете статус кодове, които да се считат за успешен отговор.",
@@ -95,7 +95,7 @@ export default {
"Repeat New Password": "Повторете новата парола",
"Update Password": "Актуализирай паролата",
"Disable Auth": "Изключи удостоверяване",
"Enable Auth": "Включи удостоверяване",
"Enable Auth": "Активирай удостоверяване",
"disableauth.message1": "Сигурни ли сте, че желаете да <strong>изключите удостоверяването</strong>?",
"disableauth.message2": "Използва се в случаите, когато <strong>има настроен алтернативен метод за удостоверяване</strong> преди Uptime Kuma, например Cloudflare Access, Authelia или друг механизъм за удостоверяване.",
"Please use this option carefully!": "Моля, използвайте с повишено внимание.",
@@ -126,7 +126,7 @@ export default {
Import: "Импорт",
respTime: "Време за отговор (ms)",
notAvailableShort: "Няма",
"Default enabled": "Включен по подразбиране",
"Default enabled": "Активирано по подразбиране",
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори",
Create: "Създай",
"Clear Data": "Изтрий данни",
@@ -145,8 +145,8 @@ export default {
"Keep both": "Запази двете",
"Verify Token": "Провери токен код",
"Setup 2FA": "Настройка 2FA",
"Enable 2FA": "Включи 2FA",
"Disable 2FA": "Изключи 2FA",
"Enable 2FA": "Активирай 2FA",
"Disable 2FA": "Деактивирай 2FA",
"2FA Settings": "Настройка за 2FA",
"Two Factor Authentication": "Двуфакторно удостоверяване",
Active: "Активно",
@@ -194,7 +194,7 @@ export default {
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Поддържа 50+ услуги за инвестяване)",
apprise: "Apprise (Поддържа 50+ услуги за известяване)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
@@ -281,19 +281,19 @@ export default {
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
"Icon URL": "URL адрес за иконка",
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
aboutMattermostChannelName: "Може да замените канала по подразбиране, към който публикува уеб куката, като въведете името на канала в полето \"Канал име\". Tрябва да бъде активирано в настройките за уеб кука на Mattermost. Например: #other-channel",
aboutMattermostChannelName: "Може да замените канала по подразбиране, към който публикува уеб куката, като въведете името на канала в полето \"Канал име\". Трябва да бъде активирано в настройките за уеб кука на Mattermost. Например: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - евтин, но бавен. Често е претоварен. Само за получатели от Полша.",
promosmsTypeFlash: "SMS FLASH - Съобщението автоматично се показва на устройството на получателя. Само за получатели от Полша.",
promosmsTypeFull: "SMS FULL - Високо ниво на SMS услуга. Може да използвате Вашето име като подател (Необходимо е първо да регистрирате името). Надежден метод за съобщения тип тревога.",
promosmsTypeSpeed: "SMS SPEED - Най-висок приоритет в системата. Много бърза и надеждна, но същвременно скъпа услуга. (Около два пъти по-висока цена в сравнение с SMS FULL).",
promosmsTypeSpeed: "SMS SPEED - Най-висок приоритет в системата. Много бърза и надеждна, но същевременно скъпа услуга. (Около два пъти по-висока цена в сравнение с SMS FULL).",
promosmsPhoneNumber: "Телефонен номер (за получатели от Полша, може да пропуснете въвеждането на код за населено място)",
promosmsSMSSender: "SMS Подател име: Предварително регистрирано име или някое от имената по подразбиране: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu URL адрес за уеб кука",
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
"Internal Room Id": "ID на вътрешна стая",
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребител, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
Method: "Метод",
Body: "Съобщение",
Headers: "Хедъри",
@@ -316,7 +316,7 @@ export default {
Security: "Сигурност",
"Steam API Key": "Steam API ключ",
"Shrink Database": "Редуцирай базата данни",
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониторитане...",
"Pick a RR-Type...": "Изберете вида на ресурсния запис за мониториране...",
"Pick Accepted Status Codes...": "Изберете статус кодове, които да се считат за успешен отговор...",
Default: "По подразбиране",
"HTTP Options": "HTTP Опции",
@@ -375,12 +375,12 @@ export default {
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
Proxies: "Прокси",
default: "По подразбиране",
enabled: "Включено",
enabled: "Активирано",
setAsDefault: "Зададен по подразбиране",
deleteProxyMsg: "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
proxyDescription: "За да функционират трябва да бъдат зададени към монитор.",
enableProxyDescription: "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
setAsDefaultProxyDescription: "Това прокси ще бъде включено по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
setAsDefaultProxyDescription: "Това прокси ще бъде активно по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
"Certificate Chain": "Верига на сертификата",
Valid: "Валиден",
Invalid: "Невалиден",
@@ -432,7 +432,7 @@ export default {
Backup: "Архивиране",
About: "Относно",
wayToGetCloudflaredURL: "(Свалете \"cloudflared\" от {0})",
cloudflareWebsite: "Cloudflare уебсайт",
cloudflareWebsite: "Cloudflare уеб сайт",
"Message:": "Съобщение:",
"Don't know how to get the token? Please read the guide:": "Не знаете как да вземете токен? Моля, прочетете ръководството:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Текущата връзка може да прекъсне ако в момента сте свързани чрез \"Cloudflare Tunnel\". Сигурни ли сте, че желаете да го спрете? Въведете Вашата текуща парола за да потвърдите.",
@@ -582,4 +582,91 @@ export default {
goAlert: "GoAlert",
backupOutdatedWarning: "Отпаднало: Тъй като са добавени много функции, тази опция за архивиране не е достатъчно поддържана и не може да генерира или възстанови пълен архив.",
backupRecommend: "Моля, архивирайте дяла или папката (./data/) директно вместо това.",
Maintenance: "Поддръжка",
statusMaintenance: "Поддръжка",
"Schedule maintenance": "Планиране на поддръжка",
"Affected Monitors": "Засегнати монитори",
"Pick Affected Monitors...": "Изберете засегнати монитори...",
"Start of maintenance": "Стартирай поддръжка",
"All Status Pages": "Всички статус страници",
"Select status pages...": "Изберете статус страници...",
recurringIntervalMessage: "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
affectedMonitorsDescription: "Изберете монитори, засегнати от текущата поддръжка",
affectedStatusPages: "Покажи това съобщение за поддръжка на избрани статус страници",
atLeastOneMonitor: "Изберете поне един засегнат монитор",
deleteMaintenanceMsg: "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
Optional: "По желание",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Документация ",
"Gateway Type": "Тип на шлюза",
SMSManager: "SMSManager",
"You can divide numbers with": "Може да разделяте числата с",
or: "или",
recurringInterval: "Интервал",
Recurring: "Повтаряне",
strategyManual: "Активен/Неактивен ръчно",
warningTimezone: "Използва се часовата зона на сървъра",
weekdayShortMon: "Пон",
weekdayShortTue: "Вт",
weekdayShortWed: "Ср",
weekdayShortThu: "Чет",
weekdayShortFri: "Пет",
weekdayShortSat: "Съб",
weekdayShortSun: "Нед",
dayOfWeek: "Ден",
dayOfMonth: "Дата",
lastDay: "Последен ден",
lastDay1: "Последен ден от месеца",
lastDay2: "2-ри последен ден на месеца",
lastDay3: "3-ти последен ден на месеца",
lastDay4: "4-ти последен ден на месеца",
"No Maintenance": "Няма поддръжка",
pauseMaintenanceMsg: "Сигурни ли сте, че желаете да направите пауза?",
"maintenanceStatus-under-maintenance": "В режим поддръжка",
"maintenanceStatus-inactive": "Неактивна",
"maintenanceStatus-scheduled": "Планирана",
"maintenanceStatus-ended": "Приключена",
"maintenanceStatus-unknown": "Неизвестна",
"Display Timezone": "Покажи часова зона",
"Server Timezone": "Часова зона на сървъра",
statusPageMaintenanceEndDate: "Край",
enableGRPCTls: "Разреши изпращане на gRPC заявка с TLS връзка",
grpcMethodDescription: "Името на метода се форматира в \"cammelCase\", например sayHello, check, и т.н.",
smseagle: "SMSEagle",
smseagleTo: "Тел. номер(а)",
smseagleGroup: "Име на група/и от тел. указател",
smseagleContact: "Име(на) от тел. указател",
smseagleRecipientType: "Получател тип",
smseagleRecipient: "Получател(и) (при повече от един разделете със запетая)",
smseagleToken: "API токен за достъп",
smseagleUrl: "Вашият SMSEagle URL на устройството",
smseagleEncoding: "Изпрати като Unicode",
smseaglePriority: "Приоритет на съобщението (0-9, по подразбиране = 0)",
IconUrl: "Икона URL адрес",
webhookAdditionalHeadersTitle: "Допълнителни хедъри",
webhookAdditionalHeadersDesc: "Задава допълнителни хедъри, изпратени с уеб куката.",
"Enable DNS Cache": "Активирай DNS кеширане",
Enable: "Активирай",
Disable: "Деактивирай",
dnsCacheDescription: "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
"Single Maintenance Window": "Единичен времеви интервал за поддръжка",
"Maintenance Time Window of a Day": "Времеви интервал от деня за поддръжка",
"Effective Date Range": "Интервал от дни на влизане в сила",
"Schedule Maintenance": "Планирай поддръжка",
"Date and Time": "Дата и час",
"DateTime Range": "Изтрий времеви интервал",
Strategy: "Стратегия",
"Free Mobile User Identifier": "Free Mobile потребителски идентификатор",
"Free Mobile API Key": "Free Mobile API ключ",
"Enable TLS": "Активирай TLS",
"Proto Service Name": "Proto име на услугата",
"Proto Method": "Proto метод",
"Proto Content": "Proto съдържание",
Economy: "Икономичен",
Lowcost: "Евтин",
high: "висок",
"General Monitor Type": "Общ тип монитор",
"Passive Monitor Type": "Пасивет тип монитор",
"Specific Monitor Type": "Специфичен тип монитор",
};

View File

@@ -582,4 +582,45 @@ export default {
goAlert: "GoAlert",
backupOutdatedWarning: "Zastaralé: V poslední době byla funkčnost aplikace značně rozšířena, nicméně součást pro zálohování nepokrývá všechny možnosti. Z tohoto důvodu není možné vygenerovat úplnou zálohu a zajistit obnovení všech dat.",
backupRecommend: "Prosím, zálohujte si ručně celý svazek nebo datovou složku (./data/).",
"Optional": "Volitelný",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Docs ",
"Gateway Type": "Gateway Typ",
SMSManager: "SMSManager",
"You can divide numbers with": "Čísla můžete dělit pomocí",
"or": "nebo",
recurringInterval: "Interval",
"Recurring": "Recurring",
strategyManual: "Aktivní/Neaktivní Ručně",
warningTimezone: "Používá se časové pásmo serveru",
weekdayShortMon: "Po",
weekdayShortTue: "Út",
weekdayShortWed: "St",
weekdayShortThu: "Čt",
weekdayShortFri: "Pá",
weekdayShortSat: "So",
weekdayShortSun: "Ne",
dayOfWeek: "Den v týdnu",
dayOfMonth: "Den v měsíci",
lastDay: "Poslední den",
lastDay1: "1. poslední den v měsíci",
lastDay2: "2. poslední den v měsíci",
lastDay3: "3. poslední den v měsíci",
lastDay4: "4. poslední den v měsíci",
"No Maintenance": "Žádna údržba",
pauseMaintenanceMsg: "Jsi si jistý, že chceš pozastavit údržbu?",
"maintenanceStatus-under-maintenance": "Údržba",
"maintenanceStatus-inactive": "Neaktivní",
"maintenanceStatus-scheduled": "Naplánováno",
"maintenanceStatus-ended": "Ukončeno",
"maintenanceStatus-unknown": "Neznámý",
"Display Timezone": "Zobrazit časové pásmo",
"Server Timezone": "Časové pásmo serveru",
statusPageMaintenanceEndDate: "Konec",
IconUrl: "Adresa URL ikony",
"Enable DNS Cache": "Povolit DNS Cache",
"Enable": "Povolit",
"Disable": "Zakázat",
dnsCacheDescription: "V některých prostředích IPv6 nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
};

634
src/languages/de-CH.js Normal file
View File

@@ -0,0 +1,634 @@
export default {
languageName: "Deutsch (Schweiz)",
Settings: "Einstellungen",
Dashboard: "Dashboard",
"New Update": "Update verfügbar",
Language: "Sprache",
Appearance: "Erscheinungsbild",
Theme: "Erscheinungsbild",
General: "Allgemein",
Version: "Version",
"Check Update On GitHub": "Auf GitHub nach Updates suchen",
List: "Liste",
Add: "Hinzufügen",
"Add New Monitor": "Neuen Monitor hinzufügen",
"Quick Stats": "Übersicht",
Up: "Aktiv",
Down: "Inaktiv",
Pending: "Ausstehend",
Unknown: "Unbekannt",
Pause: "Pausieren",
pauseDashboardHome: "Pausiert",
Name: "Name",
Status: "Status",
DateTime: "Datum / Uhrzeit",
Message: "Nachricht",
"No important events": "Keine wichtigen Ereignisse",
Resume: "Fortsetzen",
Edit: "Bearbeiten",
Delete: "Löschen",
Current: "Aktuell",
Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf",
day: "Tag | Tage",
"-day": "-Tage",
hour: "Stunde",
"-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden",
Response: "Antwortzeit",
Ping: "Ping",
"Monitor Type": "Monitor-Typ",
Keyword: "Suchwort",
"Friendly Name": "Anzeigename",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Prüfintervall",
Retries: "Wiederholungen",
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Umgekehrter Modus",
upsideDownModeDescription: "Im umgekehrten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Statuscodes auswählen, die als erfolgreiche Verbindung gelten sollen.",
Save: "Speichern",
Notifications: "Benachrichtigungen",
"Not available, please setup.": "Nicht verfügbar, bitte einrichten.",
"Setup Notification": "Benachrichtigung einrichten",
Light: "Hell",
Dark: "Dunkel",
Auto: "Auto",
"Theme - Heartbeat Bar": "Erscheinungsbild - Zeitleiste",
Normal: "Normal",
Bottom: "Unten",
None: "Keine",
Timezone: "Zeitzone",
"Search Engine Visibility": "Sichtbarkeit für Suchmaschinen",
"Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Suchmaschinen darum bitten, die Seite nicht zu indizieren",
"Change Password": "Passwort ändern",
"Current Password": "Aktuelles Passwort",
"New Password": "Neues Passwort",
"Repeat New Password": "Neues Passwort wiederholen",
passwordNotMatchMsg: "Passwörter stimmen nicht überein.",
"Update Password": "Passwort aktualisieren",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
"disableauth.message1": "Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?",
"disableauth.message2": "Dies ist für Szenarien gedacht, <strong>in denen man eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access, Authelia oder andere Authentifizierungsmechanismen.",
"Please use this option carefully!": "Bitte mit Vorsicht nutzen.",
Logout: "Ausloggen",
notificationDescription: "Benachrichtigungen müssen einem Monitor zugewiesen werden, damit diese funktionieren.",
Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätigen",
Yes: "Ja",
No: "Nein",
Username: "Benutzername",
Password: "Passwort",
"Remember me": "Angemeldet bleiben",
Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen",
"Notification Type": "Benachrichtigungsdienst",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Zertifikatsinformation",
keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Gross-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resolverserverDescription: "Cloudflare ist als der Standardserver festgelegt. Dieser kann jederzeit geändert werden.",
"Resolver Server": "Auflösungsserver",
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis",
pauseMonitorMsg: "Bist du sicher, dass du den Monitor pausieren möchtest?",
clearEventsMsg: "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
clearHeartbeatsMsg: "Bist du sicher, dass du alle Statistiken für diesen Monitor löschen möchtest?",
"Clear Data": "Lösche Daten",
Events: "Ereignisse",
Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin-Konto",
"Repeat Password": "Passwort erneut eingeben",
"Resource Record Type": "Ressourcen Record Typ",
Export: "Export",
Import: "Import",
respTime: "Antw.-Zeit (ms)",
notAvailableShort: "N/A",
"Default enabled": "Standardmässig aktiviert",
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmässig 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 Benachrichtigungs-Token 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 jede 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, dass 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, dass du 2FA aktivieren möchtest?",
confirmDisableTwoFAMsg: "Bist du sicher, dass 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...": "Einen bestehenden Tag auswählen oder neuen hinzufügen...",
"Tag with this name already exist.": "Ein Tag mit diesem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit diesem 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": "Überprüfungsintervall",
"Resend Notification if Down X times consequently": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
resendEveryXTimes: "Erneut versenden alle {0} mal",
resendDisabled: "Erneut versenden deaktiviert",
"Import Backup": "Backup importieren",
"Export Backup": "Backup exportieren",
"Avg. Ping": "Ping ø",
"Avg. Response": "Antwortzeit ø",
"Entry Page": "Einstiegsseite",
statusPageNothing: "Noch ist hier nichts. Bitte füge eine Gruppe oder einen 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 Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite",
"Status Pages": "Status-Seiten",
telegram: "Telegram",
webhook: "Webhook",
smtp: "E-Mail (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Unterstützung für 50+ Benachrichtigungsdienste)",
GoogleChat: "Google Chat (nur Google Workspace)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Primary Base URL": "Primär URL",
"Push URL": "Push URL",
needPushEvery: "Du solltest diese URL alle {0} Sekunden aufrufen",
pushOptionalParams: "Optionale Parameter: {0}",
defaultNotificationName: "Mein {notification} Alarm ({number})",
here: "hier",
Required: "Erforderlich",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "Hier kannst du einen Token erhalten {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
wayToGetTelegramChatID: "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
chatIDNotFound: "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} ist gut für alle modernen HTTP-Server, wie z.B. Express.js, geeignet",
webhookFormDataDesc: "{multipart} ist gut für PHP. Das JSON muss mit {decodeFunction} verarbeitet werden",
secureOptionNone: "Keine / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "TLS-Fehler ignorieren",
"From Email": "Absender E-Mail",
emailCustomSubject: "Benutzerdefinierter Betreff",
"To Email": "Empfänger E-Mail",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
"Bot Display Name": "Bot-Anzeigename",
"Prefix Custom Message": "Benutzerdefinierter Nachrichten Präfix",
"Hello @everyone is...": "Hallo {'@'}everyone ist...",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Wie eine Webhook-URL erstellt werden kann, erfährst du {0}.",
Number: "Nummer",
Recipients: "Empfänger",
needSignalAPI: "Es wird ein Signal Client mit REST-API benötigt.",
wayToCheckSignalURL: "Du kannst diese URL aufrufen, um zu sehen, wie du eine einrichtest:",
signalImportant: "WICHTIG: Gruppen und Nummern können in Empfängern nicht gemischt werden!",
"Application Token": "Anwendungstoken",
"Server URL": "Server URL",
Priority: "Priorität",
"Icon Emoji": "Icon Emoji",
"Channel Name": "Kanalname",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Weitere Informationen zu Webhooks auf: {0}",
aboutChannelName: "Gebe den Kanalnamen ein in {0} Feld Kanalname, falls du den Webhook-Kanal umgehen möchtest. Ex: #other-channel",
aboutKumaURL: "Wenn das Feld für die Uptime Kuma URL leer gelassen wird, wird standardmässig die GitHub Projekt Seite verwendet.",
emojiCheatSheet: "Emoji Cheat Sheet: {0}",
"User Key": "Benutzerschlüssel",
Device: "Gerät",
"Message Title": "Nachrichtentitel",
"Notification Sound": "Benachrichtigungston",
"More info on:": "Mehr Infos auf: {0}",
pushoverDesc1: "Notfallpriorität (2) hat standardmässig 30 Sekunden Auszeit zwischen den Versuchen und läuft nach 1 Stunde ab.",
pushoverDesc2: "Fülle das Geräte Feld aus, wenn du Benachrichtigungen an verschiedene Geräte senden möchtest.",
"SMS Type": "SMS Typ",
octopushTypePremium: "Premium (Schnell - zur Benachrichtigung empfohlen)",
octopushTypeLowCost: "Kostengünstig (Langsam - manchmal vom Betreiber gesperrt)",
checkPrice: "Prüfe {0} Preise:",
octopushLegacyHint: "Verwendest du die Legacy-Version von Octopush (2011-2020) oder die neue Version?",
"Check octopush prices": "Vergleiche die Oktopush Preise {0}.",
octopushPhoneNumber: "Telefonnummer (Internationales Format, z.B : +49612345678) ",
octopushSMSSender: "Name des SMS-Absenders : 3-11 alphanumerische Zeichen und Leerzeichen (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Geräte ID",
"Apprise URL": "Apprise URL",
"Example:": "Beispiel: {0}",
"Read more:": "Weiterlesen: {0}",
"Status:": "Status: {0}",
"Read more": "Weiterlesen",
appriseInstalled: "Apprise ist installiert.",
appriseNotInstalled: "Apprise ist nicht installiert. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
"Icon URL": "Icon URL",
aboutIconURL: "Du kannst einen Link zu einem Bild in 'Icon URL' übergeben um das Standardprofilbild zu überschreiben. Wird nicht verwendet, wenn ein Icon Emoji gesetzt ist.",
aboutMattermostChannelName: "Du kannst den Standardkanal, auf dem der Webhook gesendet wird überschreiben, indem der Kanalnamen in das Feld 'Channel Name' eingeben wird. Dies muss in den Mattermost Webhook-Einstellungen aktiviert werden. Ex: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - billig, aber langsam und oft überladen. Auf polnische Empfänger beschränkt.",
promosmsTypeFlash: "SMS FLASH - Die Nachricht wird automatisch auf dem Empfängergerät angezeigt. Auf polnische Empfänger beschränkt.",
promosmsTypeFull: "SMS FULL - Premium Stufe von SMS, es kann der Absendernamen verwendet werden (Der Name musst zuerst registriert werden). Zuverlässig für Warnungen.",
promosmsTypeSpeed: "SMS SPEED - Höchste Priorität im System. Sehr schnell und zuverlässig, aber teuer (Ungefähr das doppelte von SMS FULL).",
promosmsPhoneNumber: "Telefonnummer (für polnische Empfänger können die Vorwahlen übersprungen werden)",
promosmsSMSSender: "Name des SMS-Absenders : vorregistrierter Name oder einer der Standardwerte: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu Webhook URL",
matrixHomeserverURL: "Heimserver URL (mit http(s):// und optionalen Ports)",
"Internal Room Id": "Interne Raum-ID",
matrixDesc1: "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Es wird dringend empfohlen einen neuen Benutzer anzulegen und nicht den Zugriffstoken deines eigenen Matrix-Benutzers zu verwenden. Anderenfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du Folgendes ausführst {0}",
Method: "Method",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "Der Header ist kein gültiges JSON: ",
BodyInvalidFormat: "Der Body ist kein gültiges JSON: ",
"Monitor History": "Monitor Verlauf",
clearDataOlderThan: "Bewahre die Aufzeichnungsdaten für {0} Tage auf.",
PasswordsDoNotMatch: "Passwörter stimmen nicht überein.",
records: "Einträge",
"One record": "Ein Eintrag",
steamApiKeyDescription: "Um einen Steam Game Server zu überwachen, wird ein Steam Web-API-Schlüssel benötigt. Dieser kann hier registriert werden: ",
"Current User": "Aktueller Benutzer",
recent: "Letzte",
Done: "Fertig",
Info: "Info",
Security: "Sicherheit",
"Steam API Key": "Steam API Key",
"Shrink Database": "Datenbank verkleinern",
"Pick a RR-Type...": "Wähle ein RR-Typ aus...",
"Pick Accepted Status Codes...": "Wähle akzeptierte Statuscodes aus...",
Default: "Standard",
"HTTP Options": "HTTP Optionen",
"Create Incident": "Vorfall erstellen",
Title: "Titel",
Content: "Inhalt",
Style: "Stil",
info: "info",
warning: "warnung",
danger: "gefahr",
primary: "primär",
light: "hell",
dark: "dunkel",
Post: "Eintrag",
"Please input title and content": "Bitte Titel und Inhalt eingeben",
Created: "Erstellt",
"Last Updated": "Zuletzt aktualisiert",
Unpin: "Loslösen",
"Switch to Light Theme": "Zu hellem Thema wechseln",
"Switch to Dark Theme": "Zum dunklen Thema wechseln",
"Show Tags": "Tags anzeigen",
"Hide Tags": "Tags ausblenden",
Description: "Beschreibung",
"No monitors available.": "Keine Monitore verfügbar.",
"Add one": "Hinzufügen",
"No Monitors": "Keine Monitore",
"Untitled Group": "Gruppe ohne Titel",
Services: "Dienste",
Discard: "Verwerfen",
Cancel: "Abbrechen",
"Powered by": "Powered by",
shrinkDatabaseDescription: "Löse VACUUM für die SQLite Datenbank aus. Wenn die Datenbank nach 1.10.0 erstellt wurde, ist AUTO_VACUUM bereits aktiviert und diese Aktion ist nicht erforderlich.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Benutzername (inkl. webapi_ prefix)",
serwersmsAPIPassword: "API Passwort",
serwersmsPhoneNumber: "Telefonnummer",
serwersmsSenderName: "Name des SMS-Absenders (über Kundenportal registriert)",
stackfield: "Stackfield",
clicksendsms: "ClickSend SMS",
apiCredentials: "API Zugangsdaten",
smtpDkimSettings: "DKIM Einstellungen",
smtpDkimDesc: "Details zur Konfiguration sind in der Nodemailer DKIM {0} zu finden.",
documentation: "Dokumentation",
smtpDkimDomain: "Domain Name",
smtpDkimKeySelector: "Schlüssel Auswahl",
smtpDkimPrivateKey: "Privater Schlüssel",
smtpDkimHashAlgo: "Hash-Algorithmus (Optional)",
smtpDkimheaderFieldNames: "Zu validierende Header-Schlüssel (optional)",
smtpDkimskipFields: "Zu ignorierende Header Schlüssel (optional)",
PushByTechulus: "Push by Techulus",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpunkt",
alertaEnvironment: "Umgebung",
alertaApiKey: "API Schlüssel",
alertaAlertState: "Alarmstatus",
alertaRecoverState: "Wiederherstellungsstatus",
deleteStatusPageMsg: "Bist du sicher, dass du diese Status-Seite löschen willst?",
Proxies: "Proxies",
default: "Standard",
enabled: "Aktiviert",
setAsDefault: "Als Standard setzen",
deleteProxyMsg: "Bist du sicher, dass du diesen Proxy für alle Monitore löschen willst?",
proxyDescription: "Proxies müssen einem Monitor zugewiesen werden, um zu funktionieren.",
enableProxyDescription: "Dieser Proxy wird keinen Effekt auf Monitor-Anfragen haben, bis er aktiviert ist. Du kannst ihn temporär von allen Monitoren nach Aktivierungsstatus deaktivieren.",
setAsDefaultProxyDescription: "Dieser Proxy wird standardmässig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immer noch für jeden Monitor einzeln deaktivieren.",
"Certificate Chain": "Zertifikatskette",
Valid: "Gültig",
Invalid: "Ungültig",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "Telefonnummern",
TemplateCode: "Vorlagencode",
SignName: "Signaturname",
"Sms template must contain parameters: ": "SMS Vorlage muss folgende Parameter enthalten: ",
"Bark Endpoint": "Bark Endpunkt",
WebHookUrl: "Webhook URL",
SecretKey: "Geheimer Schlüssel",
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
"Device Token": "Gerätetoken",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Hoch",
Retry: "Wiederholungen",
Topic: "Thema",
"WeCom Bot Key": "WeCom Bot Schlüssel",
"Setup Proxy": "Proxy einrichten",
"Proxy Protocol": "Proxy Protokoll",
"Proxy Server": "Proxy-Server",
"Proxy server has authentication": "Proxy-Server hat Authentifizierung",
User: "Benutzer",
Installed: "Installiert",
"Not installed": "Nicht installiert",
Running: "Läuft",
"Not running": "Gestoppt",
"Remove Token": "Token entfernen",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Neue Status-Seite hinzufügen",
Slug: "Slug",
"Accept characters:": "Akzeptierte Zeichen:",
startOrEndWithOnly: "Nur mit {0} anfangen und enden",
"No consecutive dashes": "Keine aufeinanderfolgenden Bindestriche",
Next: "Weiter",
"The slug is already taken. Please choose another slug.": "Der Slug ist bereits in Verwendung. Bitte wähle einen anderen.",
"No Proxy": "Kein Proxy",
Authentication: "Authentifizierung",
"HTTP Basic Auth": "HTTP Basisauthentifizierung",
"New Status Page": "Neue Status-Seite",
"Page Not Found": "Seite nicht gefunden",
"Reverse Proxy": "Reverse Proxy",
Backup: "Sicherung",
About: "Über",
wayToGetCloudflaredURL: "(Lade Cloudflare von {0} herunter)",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Nachricht:",
"Don't know how to get the token? Please read the guide:": "Du weisst nicht, wie man den Token bekommt? Lies die Anleitung dazu:",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Die aktuelle Verbindung kann unterbrochen werden, wenn du aktuell über Cloudflare Tunnel verbunden bist. Bist du sicher, dass du es stoppen willst? Gib zur Bestätigung dein aktuelles Passwort ein.",
"Other Software": "Andere Software",
"For example: nginx, Apache and Traefik.": "Zum Beispiel: nginx, Apache und Traefik.",
"Please read": "Bitte lesen",
"Subject:": "Betreff:",
"Valid To:": "Gültig bis:",
"Days Remaining:": "Tage verbleibend:",
"Issuer:": "Aussteller:",
"Fingerprint:": "Fingerabdruck:",
"No status pages": "Keine Status-Seiten",
"Domain Name Expiry Notification": "Benachrichtigung bei Ablauf des Domainnamens",
Customize: "Anpassen",
"Custom Footer": "Eigener Footer",
"Custom CSS": "Eigenes CSS",
"Footer Text": "Fusszeile",
"Show Powered By": "Zeige 'Powered By'",
"Date Created": "Erstellt am",
"Domain Names": "Domainnamen",
signedInDisp: "Angemeldet als {0}",
signedInDispDisabled: "Authentifizierung deaktiviert.",
dnsPortDescription: "DNS server port. Standard ist 53. Der Port kann jederzeit geändert werden.",
topic: "Thema",
topicExplanation: "MQTT Thema für den monitor",
successMessage: "Erfolgsnachricht",
successMessageExplanation: "MQTT Nachricht, die als Erfolg angesehen wird",
error: "Fehler",
critical: "kritisch",
wayToGetPagerDutyKey: "Dieser kann unter Service -> Service Directory -> (Select a service) -> Integrations -> Add integration gefunden werden. Hier muss nach \"Events API V2\" gesucht werden. Mehr informationen {0}",
"Integration Key": "Schlüssel der Integration",
"Integration URL": "URL der Integration",
"Auto resolve or acknowledged": "Automatisch lösen oder bestätigen",
"do nothing": "nichts tun",
"auto acknowledged": "automatisch bestätigen",
"auto resolve": "automatisch lösen",
"Bark Group": "Bark Gruppe",
"Bark Sound": "Bark Klang",
"HTTP Headers": "HTTP Kopfzeilen",
"Trust Proxy": "Vertrauenswürdiger Proxy",
Proxy: "Proxy",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Adresse",
onebotMessageType: "OneBot Nachrichtentyp",
onebotGroupMessage: "Gruppe",
onebotPrivateMessage: "Privat",
onebotUserOrGroupId: "Gruppe/Nutzer ID",
onebotSafetyTips: "Zur Sicherheit ein access token setzen",
"PushDeer Key": "PushDeer Schlüssel",
RadiusSecret: "Radius Geheimnis",
RadiusSecretDescription: "Geteiltes Geheimnis zwischen Client und Server",
RadiusCalledStationId: "ID der angesprochenen Station",
RadiusCalledStationIdDescription: "Identifikation des angesprochenen Geräts",
RadiusCallingStationId: "ID der ansprechenden Station",
RadiusCallingStationIdDescription: "Identifikation des ansprechenden Geräts",
"Certificate Expiry Notification": "Benachrichtigung ablaufendes Zertifikat",
"API Username": "API Nutzername",
"API Key": "API Schlüssel",
"Recipient Number": "Empfängernummer",
"From Name/Number": "Von Name/Nummer",
"Leave blank to use a shared sender number.": "Leer lassen um eine geteilte Absendernummer zu nutzen.",
"Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "Endpunkt",
octopushAPIKey: "\"API Schlüssel\" der HTTP API Zugangsdaten im control panel",
octopushLogin: "\"Login\" der HTTP API Zugangsdaten im control panel",
promosmsLogin: "API Login Name",
promosmsPassword: "API Password",
"pushoversounds pushover": "Pushover (Standard)",
"pushoversounds bike": "Fahrrad",
"pushoversounds bugle": "Signalhorn",
"pushoversounds cashregister": "Kasse",
"pushoversounds classical": "Klassisch",
"pushoversounds cosmic": "Kosmisch",
"pushoversounds falling": "Abfallend",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Eingang",
"pushoversounds intermission": "Pause",
"pushoversounds magic": "Magisch",
"pushoversounds mechanical": "Mechanisch",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Sirene",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Schlepper Horn",
"pushoversounds alien": "Ausserirdisch (lang)",
"pushoversounds climb": "Ansteigende (lang)",
"pushoversounds persistent": "Hartnäckig (lang)",
"pushoversounds echo": "Pushover Echo (lang)",
"pushoversounds updown": "Auf und Ab (lang)",
"pushoversounds vibrate": "Nur vibrieren",
"pushoversounds none": "Nichts (Stille)",
pushyAPIKey: "Geheimer API Schlüssel",
pushyToken: "Gerätetoken",
"Show update if available": "Verfügbare Updates anzeigen",
"Also check beta release": "Auch nach beta Versionen schauen",
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?",
"Check how to config it for WebSocket": "Prüfen, wie er für die Nutzung mit WebSocket konfiguriert wird",
"Steam Game Server": "Steam Game Server",
"Most likely causes:": "Wahrscheinliche Ursachen:",
"The resource is no longer available.": "Die Quelle ist nicht mehr verfügbar.",
"There might be a typing error in the address.": "Es gibt einen Tippfehler in der Adresse.",
"What you can try:": "Was du versuchen kannst:",
"Retype the address.": "Schreibe die Adresse erneut.",
"Go back to the previous page.": "Gehe zur vorigen Seite.",
"Coming Soon": "Kommt bald",
wayToGetClickSendSMSToken: "Du kannst einen API Nutzernamen und Schlüssel unter {0} erhalten.",
"Connection String": "Verbindungstext",
Query: "Abfrage",
settingsCertificateExpiry: "TLS Zertifikatsablauf",
certificationExpiryDescription: "HTTPS Monitore senden eine Benachrichtigung, wenn das Zertifikat abläuft in:",
"Setup Docker Host": "Docker Host einrichten",
"Connection Type": "Verbindungstyp",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "Bist du sicher diesen docker host für alle Monitore zu löschen?",
socket: "Socket",
tcp: "TCP / HTTP",
"Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Thema",
Domain: "Domain",
Workstation: "Workstation",
disableCloudflaredNoAuthMsg: "Du bist im nicht-authentifizieren Modus, ein Passwort wird nicht benötigt.",
trustProxyDescription: "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.",
wayToGetLineNotifyToken: "Du kannst hier ein Token erhalten: {0}",
Examples: "Beispiele",
"Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Lange gültiges Access Token",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Lange gültige Access Token können durch klicken auf den Profilnamen (unten links) und dann einen Klick auf Create Token am Ende erstellt werden. ",
"Notification Service": "Benachrichtigungsdienst",
"default: notify all devices": "standard: Alle Geräte benachrichtigen",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Eine Liste der Benachrichtigungsdienste kann im Home Assistant unter \"Developer Tools > Services\" gefunden werden, wnen man nach \"notification\" sucht um den Geräte-/Telefonnamen zu finden.",
"Automations can optionally be triggered in Home Assistant:": "Automatisierungen können optional im Home Assistant ausgelöst werden:",
"Trigger type:": "Auslöser:",
"Event type:": "Ereignistyp:",
"Event data:": "Ereignis daten:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
"Frontend Version": "Frontend Version",
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
Maintenance: "Wartung",
statusMaintenance: "Wartung",
"Schedule maintenance": "Geplante Wartung",
"Affected Monitors": "Betroffene Monitore",
"Pick Affected Monitors...": "Wähle betroffene Monitore...",
"Start of maintenance": "Beginn der Wartung",
"All Status Pages": "Alle Status Seiten",
"Select status pages...": "Wähle Status Seiten...",
recurringIntervalMessage: "einmal pro Tag ausgeführt | Wird alle {0} Tage ausgführt",
affectedMonitorsDescription: "Wähle alle Monitore die von der Wartung betroffen sind",
affectedStatusPages: "Zeige diese Nachricht auf ausgewählten Status Seiten",
atLeastOneMonitor: "Wähle mindestens einen Monitor",
deleteMaintenanceMsg: "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL",
goAlertInfo: "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
goAlertIntegrationKeyInfo: "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
goAlert: "GoAlert",
backupOutdatedWarning: "Veraltet: Eine menge Neuerungen sind eingeflossen und diese Funktion wurde etwas vernachlässigt worden. Es kann kein vollständiges Backup erstellt oder eingespielt werden.",
backupRecommend: "Bitte Backup das Volume oder den Ordner (./ data /) selbst.",
Optional: "Optional",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Dokumente",
"Gateway Type": "Gateway Type",
SMSManager: "SMSManager",
"You can divide numbers with": "Du kannst Zahlen teilen mit",
or: "oder",
recurringInterval: "Intervall",
Recurring: "Wiederkehrend",
strategyManual: "Active/Inactive Manually",
warningTimezone: "Es wird die Zeitzone des Servers genutzt",
weekdayShortMon: "Mo",
weekdayShortTue: "Di",
weekdayShortWed: "Mi",
weekdayShortThu: "Do",
weekdayShortFri: "Fr",
weekdayShortSat: "Sa",
weekdayShortSun: "So",
dayOfWeek: "Tag der Woche",
dayOfMonth: "Tag im Monat",
lastDay: "Letzter Tag",
lastDay1: "Letzter Tag im Monat",
lastDay2: "Vorletzer Tag im Monat",
lastDay3: "3. letzter Tag im Monat",
lastDay4: "4. letzter Tag im Monat",
"No Maintenance": "Keine Wartung",
pauseMaintenanceMsg: "Möchtest du wirklich pausieren?",
"maintenanceStatus-under-maintenance": "Unter Wartung",
"maintenanceStatus-inactive": "Inaktiv",
"maintenanceStatus-scheduled": "Geplant",
"maintenanceStatus-ended": "Ende",
"maintenanceStatus-unknown": "Unbekannt",
"Display Timezone": "Zeitzone anzeigen",
"Server Timezone": "Server Zeitzone",
statusPageMaintenanceEndDate: "Ende",
};

View File

@@ -96,7 +96,7 @@ export default {
"Notification Type": "Benachrichtigungsdienst",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Zertifikatsinfo",
"Certificate Info": "Zertifikatsinformation",
keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
@@ -181,7 +181,7 @@ export default {
"Degraded Service": "Eingeschränkter Dienst",
"Add Group": "Gruppe hinzufügen",
"Add a monitor": "Monitor hinzufügen",
"Edit Status Page": "Bearbeite Status-Seite",
"Edit Status Page": "Statusseite bearbeiten",
"Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite",
"Status Pages": "Status-Seiten",
@@ -204,7 +204,7 @@ export default {
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Primary Base URL": "Primär URL",
"Primary Base URL": "Primäre Basis-URL",
"Push URL": "Push URL",
needPushEvery: "Du solltest diese URL alle {0} Sekunden aufrufen",
pushOptionalParams: "Optionale Parameter: {0}",
@@ -383,7 +383,7 @@ export default {
deleteProxyMsg: "Bist du sicher, dass du diesen Proxy für alle Monitore löschen willst?",
proxyDescription: "Proxies müssen einem Monitor zugewiesen werden, um zu funktionieren.",
enableProxyDescription: "Dieser Proxy wird keinen Effekt auf Monitor-Anfragen haben, bis er aktiviert ist. Du kannst ihn temporär von allen Monitoren nach Aktivierungsstatus deaktivieren.",
setAsDefaultProxyDescription: "Dieser Proxy wird standardmäßig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immernoch für jeden Monitor einzeln deaktivieren.",
setAsDefaultProxyDescription: "Dieser Proxy wird standardmäßig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immer noch für jeden Monitor einzeln deaktivieren.",
"Certificate Chain": "Zertifikatskette",
Valid: "Gültig",
Invalid: "Ungültig",
@@ -496,7 +496,7 @@ export default {
"API Key": "API Schlüssel",
"Recipient Number": "Empfängernummer",
"From Name/Number": "Von Name/Nummer",
"Leave blank to use a shared sender number.": "Leer lassen um eine geteilte Sendernummer zu nutzen.",
"Leave blank to use a shared sender number.": "Leer lassen um eine geteilte Absendernummer zu nutzen.",
"Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM",
endpoint: "Endpunkt",
@@ -530,7 +530,7 @@ export default {
pushyAPIKey: "Geheimer API Schlüssel",
pushyToken: "Gerätetoken",
"Show update if available": "Verfügbare Updates anzeigen",
"Also check beta release": "Auch nach beta Versionen schauen",
"Also check beta release": "Auch nach Beta Versionen schauen",
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?",
"Check how to config it for WebSocket": "Prüfen, wie er für die Nutzung mit WebSocket konfiguriert wird",
"Steam Game Server": "Steam Game Server",
@@ -568,12 +568,74 @@ export default {
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Lange gültige Access Token können durch klicken auf den Profilnamen (unten links) und dann einen Klick auf Create Token am Ende erstellt werden. ",
"Notification Service": "Benachrichtigungsdienst",
"default: notify all devices": "standard: Alle Geräte benachrichtigen",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Eine Liste der Benachrichtigungsdiesnte kann im Home Assistant unter \"Developer Tools > Services\" gefunden werden, wnen man nach \"notification\" sucht um den Geräte-/Telefonnamen zu finden.",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "Eine Liste der Benachrichtigungsdienste kann im Home Assistant unter \"Developer Tools > Services\" gefunden werden, wnen man nach \"notification\" sucht um den Geräte-/Telefonnamen zu finden.",
"Automations can optionally be triggered in Home Assistant:": "Automatisierungen können optional im Home Assistant ausgelöst werden:",
"Trigger type:": "Auslösertyp:",
"Trigger type:": "Auslöser:",
"Event type:": "Ereignistyp:",
"Event data:": "Ereignis daten:",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
"Frontend Version": "Frontend Version",
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
Maintenance: "Wartung",
statusMaintenance: "Wartung",
"Schedule maintenance": "Geplante Wartung",
"Affected Monitors": "Betroffene Monitore",
"Pick Affected Monitors...": "Wähle betroffene Monitore...",
"Start of maintenance": "Beginn der Wartung",
"All Status Pages": "Alle Status Seiten",
"Select status pages...": "Statusseiten auswählen...",
recurringIntervalMessage: "Einmal pro Tag ausgeführt | Wird alle {0} Tage ausgführt",
affectedMonitorsDescription: "Wähle Monitore aus, die von der aktuellen Wartung betroffen sind",
affectedStatusPages: "Diese Wartungsmeldung auf ausgewählten Statusseiten anzeigen",
atLeastOneMonitor: "Wähle mindestens einen Monitor",
deleteMaintenanceMsg: "Möchtest du diese Wartung löschen?",
"Base URL": "Basis URL",
goAlertInfo: "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
goAlertIntegrationKeyInfo: "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
goAlert: "GoAlert",
backupOutdatedWarning: "Veraltet: Da viele Funktionen hinzugefügt wurden und diese Sicherungsfunktion nicht mehr gepflegt wird, kann sie keine vollständige Sicherung erstellen oder wiederherstellen.",
backupRecommend: "Bitte sichere stattdessen das Volume oder den Datenordner (./data/) direkt.",
Optional: "Optional",
squadcast: "Squadcast",
SendKey: "SendKey",
"SMSManager API Docs": "SMSManager API Dokumente",
"Gateway Type": "Gateway Type",
SMSManager: "SMSManager",
"You can divide numbers with": "Du kannst Zahlen teilen mit",
or: "oder",
recurringInterval: "Intervall",
Recurring: "Wiederkehrend",
"Single Maintenance Window": "Einzigartiges Wartungsfenster",
"Maintenance Time Window of a Day": "Zeitfenster für die Wartung",
"Effective Date Range": "Bereich der Wirksamkeitsdaten",
strategyManual: "Aktiv/Inaktiv Manuell",
warningTimezone: "Es wird die Zeitzone des Servers verwendet",
weekdayShortMon: "Mo",
weekdayShortTue: "Di",
weekdayShortWed: "Mi",
weekdayShortThu: "Do",
weekdayShortFri: "Fr",
weekdayShortSat: "Sa",
weekdayShortSun: "So",
dayOfWeek: "Tag der Woche",
dayOfMonth: "Tag im Monat",
lastDay: "Letzter Tag",
lastDay1: "Letzter Tag im Monat",
lastDay2: "Vorletzer Tag im Monat",
lastDay3: "3. letzter Tag im Monat",
lastDay4: "4. letzter Tag im Monat",
"No Maintenance": "Keine Wartung",
"Schedule Maintenance": "Wartung planen",
pauseMaintenanceMsg: "Möchtest du wirklich pausieren?",
"maintenanceStatus-under-maintenance": "Unter Wartung",
"maintenanceStatus-inactive": "Inaktiv",
"maintenanceStatus-scheduled": "Geplant",
"maintenanceStatus-ended": "Ende",
"maintenanceStatus-unknown": "Unbekannt",
"Display Timezone": "Zeitzone anzeigen",
"Server Timezone": "Server Zeitzone",
"Date and Time": "Datum und Zeit",
"DateTime Range": "Datums- und Zeitbereich",
Strategy: "Strategie",
statusPageMaintenanceEndDate: "Ende",
};

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