Compare commits

...

144 Commits
1.5.3 ... 1.6.0

Author SHA1 Message Date
LouisLam
a49df29a87 update to 1.6.0 2021-09-10 17:56:33 +08:00
LouisLam
08de0090dc add a better approach for patching db, change setting.value from varchar to TEXT, restore Database.close() to 1.2.0 2021-09-10 17:23:45 +08:00
Louis Lam
97df200d6c Merge pull request #365 from Ponkhy/master
Minor translation fixes
2021-09-10 11:46:20 +08:00
Louis Lam
d5b17a3778 Merge pull request #367 from xinac721/master
update Simplified Chinese language(更新简体中文语言)
2021-09-10 11:45:57 +08:00
新逸Cary
2f0d994c83 update Simplified Chinese language(更新简体中文语言) 2021-09-10 09:26:01 +08:00
新逸Cary
e683033f4e Merge pull request #2 from louislam/master
update
2021-09-10 08:52:38 +08:00
Ponkhy
c62732fa9c Minor translation fixes 2021-09-10 01:24:21 +02:00
Louis Lam
2583dbe0e0 Update README.md 2021-09-09 22:58:31 +08:00
LouisLam
ca479673e7 avoid Chrome duplicate id warning 2021-09-09 21:28:27 +08:00
LouisLam
573c7faddd switch on the notification, if it is added in EditMonitor.vue 2021-09-09 21:24:29 +08:00
LouisLam
7a4432de1e better switch button color in dark mode 2021-09-09 20:45:58 +08:00
LouisLam
94a6c1a344 update language files 2021-09-09 20:38:19 +08:00
Louis Lam
ee9e48fe53 Merge pull request #361 from iomataani/italian-translation-update
Italian translation update
2021-09-09 20:26:07 +08:00
LouisLam
a4aee49b03 Merge remote-tracking branch 'origin/master' 2021-09-09 20:22:44 +08:00
LouisLam
e330875c80 fix restore fail if no isDefault 2021-09-09 20:22:32 +08:00
Paride Barison
44eba70b78 altri errorini sistemati 2021-09-09 14:19:15 +02:00
Paride Barison
c6dab944d1 altra correzione 2021-09-09 14:12:30 +02:00
Ioma Taani
0f4bc5850b Merge branch 'louislam:master' into italian-translation-update 2021-09-09 14:05:00 +02:00
Louis Lam
f37190a73d Update README.md 2021-09-09 18:02:58 +08:00
Louis Lam
29f96fae93 Update README.md 2021-09-09 18:01:15 +08:00
Louis Lam
331ae5ec20 Merge pull request #306 from Ponkhy/import-export
Added import and export function
2021-09-09 16:15:48 +08:00
LouisLam
8ee34c7904 also handle the first beat problem in uptime 2021-09-09 15:55:34 +08:00
LouisLam
4f07c2ea9a uptime calculation fully sum in sql 2021-09-09 15:46:28 +08:00
LouisLam
0b1d0d22ff add indexes, improve queries 2021-09-09 12:43:18 +08:00
LouisLam
24facc79d7 sendHeartbeatList use raw data 2021-09-09 11:57:23 +08:00
LouisLam
9f9c1007d7 increase sqlite cache size from 2MB to 12MB 2021-09-09 11:56:42 +08:00
LouisLam
70a6f0313c update .dockerignore 2021-09-09 11:41:43 +08:00
LouisLam
1d05df6ec9 [docker healthcheck] use retries instead, longer start-period causes starting problem in Docker Swarm mode 2021-09-09 11:41:28 +08:00
Ioma Taani
b54bbc302d migliorata la traduzione 2021-09-08 21:34:48 +02:00
LouisLam
dd283423ab improve smtp with cc, bbc and ignore tls 2021-09-09 01:13:09 +08:00
LouisLam
6006038689 fix monitor.stop() in some cases 2021-09-08 20:00:16 +08:00
LouisLam
a7b50c3630 Revert "add user-monitor-list"
This reverts commit 230a9bfaf9.
2021-09-08 19:54:38 +08:00
LouisLam
0ddbac5109 Revert "move userMonitorList out of server.js"
This reverts commit a7cf14c663.
2021-09-08 19:54:38 +08:00
LouisLam
0f440596c8 Revert "cache last heartbeat list in memory"
This reverts commit 87678ea92d.
2021-09-08 19:54:37 +08:00
LouisLam
5a0fcebd6e update link style 2021-09-08 18:59:30 +08:00
LouisLam
87678ea92d cache last heartbeat list in memory 2021-09-08 18:58:02 +08:00
LouisLam
0bf0cfa104 Merge remote-tracking branch 'origin/master' 2021-09-08 18:56:43 +08:00
LouisLam
a7cf14c663 move userMonitorList out of server.js 2021-09-08 16:27:37 +08:00
Louis Lam
325ce72a71 Merge pull request #352 from iomataani/italian-language
Italian language
2021-09-08 16:12:44 +08:00
Louis Lam
9d00bd9029 Merge pull request #357 from Saibamen/nav_from_dashHome
Route to monitor on `DashboardHome`
2021-09-08 16:11:39 +08:00
Adam Stachowicz
f6a168282e Route to monitor on DashboardHome 2021-09-08 09:55:08 +02:00
Ponkhy
acf6f1883b Fixed missing spaces in backup file 2021-09-08 09:02:24 +02:00
Paride Barison
f44f951e40 Tradotte le ultime cose come richiesto 2021-09-08 08:41:19 +02:00
Ioma Taani
dea9b05d49 Merge branch 'louislam:master' into italian-language 2021-09-08 08:30:10 +02:00
LouisLam
230a9bfaf9 add user-monitor-list 2021-09-08 14:16:30 +08:00
Louis Lam
d761d54d0e Merge pull request #340 from Ponkhy/default-notification
Added the option for default notifications
2021-09-08 12:49:14 +08:00
LouisLam
e37621853e Merge branch 'estonian'
# Conflicts:
#	src/pages/Settings.vue
2021-09-08 11:00:19 +08:00
LouisLam
f2cec60481 minor 2021-09-08 10:48:03 +08:00
LouisLam
2212cf9c08 fix confirmDisableAuth no msg if not tranlated 2021-09-08 10:28:40 +08:00
LouisLam
f7562e00c1 improve notification component, no double template 2021-09-08 10:25:03 +08:00
Ponkhy
678e248966 Added indicator while changing database records 2021-09-08 01:10:19 +02:00
Ponkhy
c1aebef005 Fixed SQL error due notification setup 2021-09-08 01:03:24 +02:00
Ponkhy
1ef4562905 Paused monitors stay paused after import 2021-09-07 23:32:25 +02:00
Ponkhy
69a7c4dab4 Improved importBackup handling 2021-09-07 23:14:44 +02:00
jtagcat
2c3880a326 Added Estonian.
- "Avg.": using ≈, as it's commonly enough known.
   There is no known shorthand for 'average' that I know of.

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

View File

@@ -20,6 +20,11 @@ yarn.lock
app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
CNAME
install.sh
SECURITY.md
tsconfig.json
### .gitignore content (commented rules are duplicated)

View File

@@ -17,7 +17,8 @@ module.exports = {
},
rules: {
"camelcase": ["warn", {
"properties": "never"
"properties": "never",
"ignoreImports": true
}],
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'

View File

@@ -27,6 +27,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
* Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
* 20 seconds interval.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
## 🔧 How to Install
@@ -106,10 +107,23 @@ Telegram Notification Sample:
If you love this project, please consider giving me a ⭐.
## 🗣️ Discussion
You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
Alternatively, you can discuss in my original post on reddit: https://www.reddit.com/r/selfhosted/comments/oi7dc7/uptime_kuma_a_fancy_selfhosted_monitoring_tool_an/
I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments.
## Contribute
If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.

View File

@@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- For sendHeartbeatList
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
-- For sendImportantHeartbeatList
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
COMMIT;

View File

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

7
db/patch9.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE notification
ADD is_default BOOLEAN default 0 NOT NULL;
COMMIT;

View File

@@ -1,25 +1,30 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS release
FROM node:14-bullseye-slim AS release
WORKDIR /app
# install dependencies
RUN apt update && apt --yes install python3 python3-pip python3-dev git g++ make iputils-ping
RUN ln -s /usr/bin/python3 /usr/bin/python
# split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
ln -s /usr/bin/python3 /usr/bin/python && \
npm install mapbox/node-sqlite3#593c9d && \
apk del .build-deps && \
rm -f /usr/bin/python
RUN npm install mapbox/node-sqlite3#593c9d
# Install apprise
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
RUN apt --yes install python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib
RUN pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
rm -rf /root/.cache
# additional package should be added here, since we don't want to re-compile the arm prebuilt again
# add sqlite3 cli for debugging in the future
RUN apt --yes install sqlite3
COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["node", "server/server.js"]
FROM release AS nightly

26
dockerfile-alpine Normal file
View File

@@ -0,0 +1,26 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS release
WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
ln -s /usr/bin/python3 /usr/bin/python && \
npm install mapbox/node-sqlite3#593c9d && \
apk del .build-deps && \
rm -f /usr/bin/python
# Install apprise
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
RUN pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

View File

@@ -1,19 +1,31 @@
let http = require("http");
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
let client;
if (process.env.SSL_KEY && process.env.SSL_CERT) {
client = require("https");
} else {
client = require("http");
}
let options = {
host: "localhost",
port: "3001",
timeout: 2000,
host: process.env.HOST || "127.0.0.1",
port: parseInt(process.env.PORT) || 3001,
timeout: 28 * 1000,
};
let request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
let request = client.request(options, (res) => {
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on("error", function (err) {
console.log("ERROR");
console.error("Health Check ERROR");
process.exit(1);
});
request.end();

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.5.3",
"version": "1.6.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -19,11 +19,11 @@
"build": "vite build",
"vite-preview-dist": "vite preview --host",
"build-docker": "npm run build-docker-alpine && npm run build-docker-debian",
"build-docker-alpine": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.5.3 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push",
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --target release . --push",
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.6.0-alpine --target release . --push",
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.6.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.6.0-debian --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
"setup": "git checkout 1.6.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",

88
server/client.js Normal file
View File

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

View File

@@ -1,15 +1,44 @@
const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { debug, sleep } = require("../src/util");
const dayjs = require("dayjs");
class Database {
static templatePath = "./db/kuma.db"
static templatePath = "./db/kuma.db";
static dataDir;
static path;
static latestVersion = 8;
/**
* @type {boolean}
*/
static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/**
* Add patch filename in key
* Values:
* true: Add it regardless of order
* false: Do nothing
* { parents: []}: Need parents before add it
*/
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
}
/**
* The finally version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature
*/
static latestVersion = 9;
static noReject = true;
static sqliteInstance = null;
static async connect() {
const acquireConnectionTimeout = 120 * 1000;
@@ -36,7 +65,11 @@ class Database {
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
await R.exec("PRAGMA cache_size = -12000");
console.log("SQLite config:");
console.log(await R.getAll("PRAGMA journal_mode"));
console.log(await R.getAll("PRAGMA cache_size"));
}
static async patch() {
@@ -56,19 +89,7 @@ class Database {
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
fs.copyFileSync(walPath, walPath + ".bak" + version);
}
this.backup(version);
// Try catch anything here, if gone wrong, restore the backup
try {
@@ -79,18 +100,92 @@ class Database {
console.info(`Patched ${sqlFile}`);
await setSetting("database_version", i);
}
console.log("Database Patched Successfully");
} catch (ex) {
await Database.close();
console.error("Patch db failed!!! Restoring the backup")
fs.copyFileSync(backupPath, Database.path);
console.error(ex)
this.restore();
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed")
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
process.exit(1);
}
}
await this.patch2();
}
/**
* Call it from patch() only
* @returns {Promise<void>}
*/
static async patch2() {
console.log("Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
debug("Patched files:");
debug(databasePatchedFiles);
try {
for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles)
}
if (this.patched) {
console.log("Database Patched Successfully");
}
} catch (ex) {
await Database.close();
this.restore();
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
process.exit(1);
}
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Used it patch2() only
* @param sqlFilename
* @param databasePatchedFiles
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
console.log(sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
console.log(sqlFilename + " is not patched");
if (value.parents) {
console.log(sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
}
this.backup(dayjs().format("YYYYMMDDHHmmss"));
console.log(sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
console.log(sqlFilename + " is patched successfully");
} else {
console.log(sqlFilename + " is already patched, skip");
}
}
/**
@@ -136,10 +231,96 @@ class Database {
* @returns {Promise<void>}
*/
static async close() {
if (this.sqliteInstance) {
this.sqliteInstance.close();
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB");
while (true) {
Database.noReject = true;
await R.close();
await sleep(2000);
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db");
}
}
console.log("SQLite closed");
process.removeListener("unhandledRejection", listener);
}
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param version
*/
static backup(version) {
if (! this.backupPath) {
console.info("Backup the db")
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
this.backupShmPath = shmPath + ".bak" + version;
fs.copyFileSync(shmPath, this.backupShmPath);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
}
}
/**
*
*/
static restore() {
if (this.backupPath) {
console.error("Patch db failed!!! Restoring the backup");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
// Delete patch failed db
try {
if (fs.existsSync(Database.path)) {
fs.unlinkSync(Database.path);
}
if (fs.existsSync(shmPath)) {
fs.unlinkSync(shmPath);
}
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
}
} catch (e) {
console.log("Restore failed, you may need to restore the backup manually");
process.exit(1);
}
// Restore backup
fs.copyFileSync(this.backupPath, Database.path);
if (this.backupShmPath) {
fs.copyFileSync(this.backupShmPath, shmPath);
}
if (this.backupWalPath) {
fs.copyFileSync(this.backupWalPath, walPath);
}
} else {
console.log("Nothing to restore");
}
console.log("Stopped database");
}
}

View File

@@ -309,7 +309,10 @@ class Monitor extends BeanModel {
previousBeat = bean;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
if (! this.isStop) {
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
}
}
beat();
@@ -317,6 +320,7 @@ class Monitor extends BeanModel {
stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
}
/**
@@ -405,58 +409,59 @@ class Monitor extends BeanModel {
static async sendUptime(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let sec = duration * 3600;
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
let heartbeatList = await R.getAll(`
SELECT duration, time, status
// Handle if heartbeat duration longer than the target duration
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
let result = await R.getRow(`
SELECT
-- SUM all duration, also trim off the beat out of time window
SUM(
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
) AS total_duration,
-- SUM all uptime duration, also trim off the beat out of time window
SUM(
CASE
WHEN (status = 1)
THEN
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
END
) AS uptime_duration
FROM heartbeat
WHERE time > DATETIME('now', ? || ' hours')
AND monitor_id = ? `, [
-duration,
WHERE time > ?
AND monitor_id = ?
`, [
startTime, startTime, startTime, startTime, startTime,
monitorID,
]);
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
let downtime = 0;
let total = 0;
let uptime;
let totalDuration = result.total_duration;
let uptimeDuration = result.uptime_duration;
let uptime = 0;
// Special handle for the first heartbeat only
if (heartbeatList.length === 1) {
if (heartbeatList[0].status === 1) {
uptime = 1;
} else {
if (totalDuration > 0) {
uptime = uptimeDuration / totalDuration;
if (uptime < 0) {
uptime = 0;
}
} else {
for (let row of heartbeatList) {
let value = parseInt(row.duration)
let time = row.time
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), "second");
value = sec - trim;
if (value < 0) {
value = 0;
}
}
total += value;
if (row.status === 0 || row.status === 2) {
downtime += value;
}
}
uptime = (total - downtime) / total;
if (uptime < 0) {
uptime = 0;
// Handle new monitor with only one beat, because the beat's duration = 0
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
console.log("here???" + status);
if (status === UP) {
uptime = 1;
}
}

View File

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

View File

@@ -0,0 +1,105 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Discord extends NotificationProvider {
name = "discord";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
let url;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == DOWN) {
let discorddowndata = {
username: discordDisplayName,
embeds: [{
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
color: 16711680,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: url,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Error",
value: heartbeatJSON["msg"],
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
let discordupdata = {
username: discordDisplayName,
embeds: [{
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
color: 65280,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Discord;

View File

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

View File

@@ -0,0 +1,60 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Line extends NotificationProvider {
name = "line";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == DOWN) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == UP) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Line;

View File

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

View File

@@ -0,0 +1,123 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Mattermost extends NotificationProvider {
name = "mattermost";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let mattermostTestData = {
username: mattermostUserName,
text: msg,
}
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
return okMsg;
}
const mattermostChannel = notification.mattermostchannel;
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
if (heartbeatJSON["status"] == DOWN) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went down.",
color: "#FF0000",
title:
"❌ " +
monitorJSON["name"] +
" service went down. ❌",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Error",
value: heartbeatJSON["msg"],
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went up!",
color: "#32CD32",
title:
"✅ " +
monitorJSON["name"] +
" service went up! ✅",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostupdata
);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Mattermost;

View File

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

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Octopush extends NotificationProvider {
name = "octopush";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let config = {
headers: {
"api-key": notification.octopushAPIKey,
"api-login": notification.octopushLogin,
"cache-control": "no-cache"
}
};
let data = {
"recipients": [
{
"phone_number": notification.octopushPhoneNumber
}
],
//octopush not supporting non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": notification.octopushSMSType,
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Octopush;

View File

@@ -0,0 +1,50 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Pushbullet extends NotificationProvider {
name = "pushbullet";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
let config = {
headers: {
"Access-Token": notification.pushbulletAccessToken,
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
let testdata = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == DOWN) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == UP) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushbullet;

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class RocketChat extends NotificationProvider {
name = "rocket.chat";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Rocket.chat testing successful.",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
"attachments": [
{
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
"title_link": notification.rocketbutton,
"text": "*Message*\n" + msg,
"color": "#32cd32"
}
]
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = RocketChat;

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
class SMTP extends NotificationProvider {
name = "smtp";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
};
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
};
}
let transporter = nodemailer.createTransport(config);
let bodyTextContent = msg;
if (heartbeatJSON) {
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
}
// send mail with defined transport object
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: msg,
text: bodyTextContent,
tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
},
});
return "Sent Successfully.";
}
}
module.exports = SMTP;

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,11 @@ debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
debug("Importing Notification");
const { Notification } = require("./notification");
Notification.init();
debug("Importing Database");
const Database = require("./database");
@@ -82,7 +85,12 @@ if (sslKey && sslCert) {
}
const io = new Server(server);
app.use(express.json())
module.exports.io = io;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client");
app.use(express.json());
/**
* Total WebSocket client connected to server currently, no actual use
@@ -519,12 +527,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
try {
checkLogin(socket)
await Notification.save(notification, notificationID, socket.userID)
let notificationBean = await Notification.save(notification, notificationID, socket.userID)
await sendNotificationList(socket)
callback({
ok: true,
msg: "Saved",
id: notificationBean.id,
});
} catch (e) {
@@ -585,6 +594,152 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
}
});
socket.on("uploadBackup", async (uploadedJSON, callback) => {
try {
checkLogin(socket)
let backupData = JSON.parse(uploadedJSON);
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`)
let notificationList = backupData.notificationList;
let monitorList = backupData.monitorList;
if (notificationList.length >= 1) {
for (let i = 0; i < notificationList.length; i++) {
let notification = JSON.parse(notificationList[i].config);
await Notification.save(notification, null, socket.userID)
}
}
if (monitorList.length >= 1) {
for (let i = 0; i < monitorList.length; i++) {
let monitor = {
name: monitorList[i].name,
type: monitorList[i].type,
url: monitorList[i].url,
interval: monitorList[i].interval,
hostname: monitorList[i].hostname,
maxretries: monitorList[i].maxretries,
port: monitorList[i].port,
keyword: monitorList[i].keyword,
ignoreTls: monitorList[i].ignoreTls,
upsideDown: monitorList[i].upsideDown,
maxredirects: monitorList[i].maxredirects,
accepted_statuscodes: monitorList[i].accepted_statuscodes,
dns_resolve_type: monitorList[i].dns_resolve_type,
dns_resolve_server: monitorList[i].dns_resolve_server,
notificationIDList: {},
}
let bean = R.dispense("monitor")
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor)
bean.user_id = socket.userID
await R.store(bean)
await updateMonitorNotification(bean.id, notificationIDList)
if (monitorList[i].active == 1) {
await startMonitor(socket.userID, bean.id);
} else {
await pauseMonitor(socket.userID, bean.id);
}
}
await sendNotificationList(socket)
await sendMonitorList(socket);
}
callback({
ok: true,
msg: "Backup successfully restored.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearEvents", async (monitorID, callback) => {
try {
checkLogin(socket)
console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`)
await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [
"",
"0",
monitorID,
]);
await sendImportantHeartbeatList(socket, monitorID, true, true);
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearHeartbeats", async (monitorID, callback) => {
try {
checkLogin(socket)
console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`)
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
monitorID
]);
await sendHeartbeatList(socket, monitorID, true, true);
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearStatistics", async (callback) => {
try {
checkLogin(socket)
console.log(`Clear Statistics User ID: ${socket.userID}`)
await R.exec("DELETE FROM heartbeat");
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
debug("added all socket handlers")
// ***************************
@@ -653,25 +808,6 @@ async function sendMonitorList(socket) {
return list;
}
async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.export())
}
io.to(socket.userID).emit("notificationList", result)
timeLogger.print("Send Notification List");
return list;
}
async function afterLogin(socket, user) {
socket.userID = user.id;
socket.join(user.id)
@@ -806,48 +942,6 @@ async function startMonitors() {
}
}
/**
* Send Heartbeat History list to socket
*/
async function sendHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID,
])
let result = [];
for (let bean of list) {
result.unshift(bean.toJSON())
}
socket.emit("heartbeatList", monitorID, result)
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`)
}
async function sendImportantHeartbeatList(socket, monitorID) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT 500
`, [
monitorID,
])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
socket.emit("importantHeartbeatList", monitorID, list)
}
async function shutdownFunction(signal) {
console.log("Shutdown requested");
console.log("Called signal: " + signal);

View File

@@ -152,7 +152,7 @@ h2 {
}
.form-switch .form-check-input {
background-color: #131a21;
background-color: #232f3b;
}
a,

View File

@@ -16,3 +16,5 @@ $dark-border-color: #1d2634;
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
$dropdown-border-radius: 0.5rem;

View File

@@ -31,7 +31,7 @@ export default {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 3, // Odd number only, even = blurry
beatMargin: 4,
move: false,
maxBeat: -1,
}
@@ -122,11 +122,25 @@ export default {
this.$root.heartbeatList[this.monitorId] = [];
}
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
if (! Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (! Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
<template>
<div class="mb-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="mb-3">
<label for="secure" class="form-label">Secure</label>
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
<option :value="false">None / STARTTLS (25, 587)</option>
<option :value="true">TLS (465)</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls-error">
Ignore TLS Error
</label>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ $t("Username") }}</label>
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="from-email" class="form-label">From Email</label>
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder="&quot;Uptime Kuma&quot; &lt;example@kuma.pet&gt;">
<div class="form-text">
</div>
</div>
<div class="mb-3">
<label for="to-email" class="form-label">To Email</label>
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
</div>
<div class="mb-3">
<label for="to-cc" class="form-label">CC</label>
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="to-bcc" class="form-label">BCC</label>
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
data() {
return {
name: "smtp",
}
},
}
</script>

View File

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

View File

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

View File

@@ -108,5 +108,24 @@ export default {
"Repeat Password": "Gentag adgangskoden",
"Resource Record Type": "Resource Record Type",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -104,9 +104,29 @@ export default {
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis",
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?",
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?",
"Clear Data": "Lösche Daten",
Events: "Ereignisse",
Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin Konto",
"Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type",
"Import/Export Backup": "Import/Export Backup",
"Export": "Export",
"Import": "Import",
respTime: "Antw. Zeit (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert",
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
Create: "Erstellen",
"Auto Get": "Auto Get",
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
"Clear all statistics": "Lösche alle Statistiken"
}

View File

@@ -16,6 +16,10 @@ export default {
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.",
rrtypeDescription: "Select the RR-Type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
Settings: "Settings",
Dashboard: "Dashboard",
"New Update": "New Update",
@@ -107,6 +111,22 @@ export default {
"Last Result": "Last Result",
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Create: "Create",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
"Clear all statistics": "Clear all Statistics"
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

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

@@ -0,0 +1,131 @@
export default {
languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
"Avg.": "≈ ",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
passwordNotMatchMsg: "Salasõnad ei kattu.",
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
pauseDashboardHome: "Seiskamine",
deleteMonitorMsg: "Kas soovid eemaldada seire?",
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded",
Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel",
Appearance: "Välimus",
Theme: "Teema",
General: "Üldine",
Version: "Versioon",
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
List: "Nimekiri",
Add: "Lisa",
"Add New Monitor": "Seire lisamine",
"Quick Stats": "Ülevaade",
Up: "Töökorras",
Down: "Rikkis",
Pending: "Määramisel",
Unknown: "Teadmata",
Pause: "Seiskamine",
Name: "Nimi",
Status: "Olek",
DateTime: "Kuupäev",
Message: "Tulemus",
"No important events": "Märkimisväärsed juhtumid puuduvad.",
Resume: "Taasta",
Edit: "Muutmine",
Delete: "Eemalda",
Current: "Hetkeseisund",
Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine",
days: "päeva",
day: "päev",
"-day": "-päev",
hour: "tund",
"-hour": "-tund",
Response: "Vastus",
Ping: "Ping",
"Monitor Type": "Seire tüüp",
Keyword: "Võtmesõna",
"Friendly Name": "Sõbralik nimi",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Tukse sagedus",
Retries: "Korduskatsed",
Advanced: "Rohkem",
"Upside Down Mode": "Tagurpidi seire",
"Max. Redirects": "Max. ümbersuunamine",
"Accepted Status Codes": "Kõlblikud HTTP koodid",
Save: "Salvesta",
Notifications: "Teavitused",
"Not available, please setup.": "Ühtegi teavitusteenust pole saadaval.",
"Setup Notification": "Lisa teavitusteenus",
Light: "hele",
Dark: "tume",
Auto: "automaatne",
"Theme - Heartbeat Bar": "Teemasäte — tuksete riba",
Normal: "tavaline",
Bottom: "all",
None: "puudub",
Timezone: "Ajatsoon",
"Search Engine Visibility": "Otsimootorite ligipääs",
"Allow indexing": "Luba indekseerimine",
"Discourage search engines from indexing site": "Keela selle saidi indekseerimine otsimootorite poolt",
"Change Password": "Muuda parooli",
"Current Password": "praegune parool",
"New Password": "uus parool",
"Repeat New Password": "korda salasõna",
"Update Password": "Uuenda salasõna",
"Disable Auth": "Lülita autentimine välja",
"Enable Auth": "Lülita autentimine sisse",
Logout: "Logi välja",
Leave: "Lahku",
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",
Confirm: "Kinnita",
Yes: "Jah",
No: "Ei",
Username: "kasutajanimi",
Password: "parool",
"Remember me": "Mäleta mind",
Login: "Logi sisse",
"No Monitors, please": "Seired puuduvad.",
"add one": "Lisa esimene",
"Notification Type": "Teavituse tüüp",
Email: "e-posti aadress",
Test: "Saada prooviteavitus",
"Certificate Info": "Sertifikaadi teave",
"Resolver Server": "Server, mis vastab DNS päringutele.",
"Resource Record Type": "DNS kirje tüüp",
"Last Result": "Viimane",
"Create your admin account": "Admininstraatori konto loomine",
"Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Create: "Create",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -66,7 +66,7 @@ export default {
"Theme - Heartbeat Bar": "Voir les services surveillés",
Normal: "Général",
Bottom: "En dessous",
None: "Non",
None: "Rien",
Timezone: "Fuseau Horaire",
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Créez votre compte administrateur",
"Repeat Password": "Répéter le mot de passe",
respTime: "Temps de réponse (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Créer",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

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

@@ -0,0 +1,131 @@
export default {
languageName: "Italiano (Italian)",
checkEverySecond: "controlla ogni {0} secondi",
"Avg.": "Media",
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
maxRedirectDescription: "Numero massimo di redirezionamenti consentito. Per disabilitare impostare \"0\".",
acceptedStatusCodesDescription: "Inserire i codici di stato considerati come risposte corrette.",
passwordNotMatchMsg: "La password non coincide.",
notificationDescription: "Assegnare la notifica a uno o più elementi monitorati per metterla in funzione.",
keywordDescription: "Cerca la parola chiave nella risposta in html o JSON e fai distinzione tra maiuscole e minuscole",
pauseDashboardHome: "In Pausa",
deleteMonitorMsg: "Si è certi di voler eliminare questo monitoraggio?",
deleteNotificationMsg: "Si è certi di voler eliminare questa notifica per tutti gli oggetti monitorati?",
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
Settings: "Impostazioni",
Dashboard: "Cruscotto",
"New Update": "Nuovo Aggiornamento Disponibile",
Language: "Lingua",
Appearance: "Aspetto",
Theme: "Tema",
General: "Generali",
Version: "Versione",
"Check Update On GitHub": "Controlla aggiornamenti su GitHub",
List: "Lista",
Add: "Aggiungi",
"Add New Monitor": "Aggiungi un nuovo monitoraggio",
"Quick Stats": "Statistiche rapide",
Up: "Su",
Down: "Giù",
Pending: "Pendente",
Unknown: "Sconosciuti",
Pause: "Metti in Pausa",
Name: "Nome",
Status: "Stato",
DateTime: "Data e Ora",
Message: "Messaggio",
"No important events": "Nessun evento importante",
Resume: "Riprendi",
Edit: "Modifica",
Delete: "Elimina",
Current: "Corrente",
Uptime: "Tempo di attività",
"Cert Exp.": "Scadenza certificato",
days: "giorni",
day: "giorno",
"-day": "-giorni",
hour: "ora",
"-hour": "-ore",
Response: "Risposta",
Ping: "Ping",
"Monitor Type": "Tipo di Monitoraggio",
Keyword: "Parola chiave",
"Friendly Name": "Nome Amichevole",
URL: "URL",
Hostname: "Nome Host",
Port: "Porta",
"Heartbeat Interval": "Intervallo di controllo",
Retries: "Tentativi",
Advanced: "Avanzate",
"Upside Down Mode": "Modalità capovolta",
"Max. Redirects": "Redirezionamenti massimi",
"Accepted Status Codes": "Codici di stato accettati",
Save: "Salva",
Notifications: "Notifiche",
"Not available, please setup.": "Non disponibili, da impostare.",
"Setup Notification": "Imposta le notifiche",
Light: "Chiaro",
Dark: "Scuro",
Auto: "Automatico",
"Theme - Heartbeat Bar": "Tema - Barra di Stato",
Normal: "Normale",
Bottom: "Sotto",
None: "Nessuna",
Timezone: "Fuso Orario",
"Search Engine Visibility": "Visibilità ai motori di ricerca",
"Allow indexing": "Permetti l'indicizzazione",
"Discourage search engines from indexing site": "Scoraggia l'indicizzazione da parte dei motori di ricerca",
"Change Password": "Cambio Password",
"Current Password": "Password Corrente",
"New Password": "Nuova Password",
"Repeat New Password": "Ripetere la nuova Password",
"Update Password": "Modifica Password",
"Disable Auth": "Disabilita l'autenticazione",
"Enable Auth": "Abilita Autenticazione",
Logout: "Esci",
Leave: "Annulla",
"I understand, please disable": "Lo capisco, disabilitare l'autenticazione.",
Confirm: "Conferma",
Yes: "Sì",
No: "No",
Username: "Nome Utente",
Password: "Password",
"Remember me": "Ricordami",
Login: "Accesso",
"No Monitors, please": "Nessun monitoraggio, cortesemente",
"add one": "aggiungerne uno",
"Notification Type": "Tipo di notifica",
Email: "E-mail",
Test: "Prova",
"Certificate Info": "Informazioni sul certificato",
"Resolver Server": "Server DNS",
"Resource Record Type": "Tipo di Resource Record",
"Last Result": "Ultimo risultato",
"Create your admin account": "Crea l'account amministratore",
"Repeat Password": "Ripeti Password",
respTime: "Tempo di Risposta (ms)",
notAvailableShort: "N/D",
Create: "Crea",
"Clear Data": "Cancella dati",
Events: "Eventi",
Heartbeats: "Controlli",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "관리자 계정 만들기",
"Repeat Password": "비밀번호 재입력",
respTime: "응답 시간 (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Maak uw beheerdersaccount aan",
"Repeat Password": "Herhaal wachtwoord",
respTime: "resp. tijd (ms)",
notAvailableShort: "N.v.t."
notAvailableShort: "N.v.t.",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

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

@@ -0,0 +1,131 @@
export default {
languageName: "Polski",
checkEverySecond: "Sprawdzaj co {0} sekund.",
"Avg.": "Średnia ",
retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",
maxRedirectDescription: "Maksymalna liczba przekierowań do wykonania. Ustaw na 0, aby wyłączyć przekierowania.",
acceptedStatusCodesDescription: "Wybierz kody stanu, które są uważane za udaną odpowiedź.",
passwordNotMatchMsg: "Powtórzone hasło nie pasuje.",
notificationDescription: "Proszę przypisać powiadomienie do monitora(ów), aby zadziałało.",
keywordDescription: "Wyszukiwanie słów kluczowych w zwykłym html lub odpowiedzi JSON. Wielkość liter ma znaczenie.",
pauseDashboardHome: "Pauza",
deleteMonitorMsg: "Czy na pewno chcesz usunąć ten monitor?",
deleteNotificationMsg: "Czy na pewno chcesz usunąć to powiadomienie dla wszystkich monitorów?",
resoverserverDescription: "Cloudflare jest domyślnym serwerem, możesz zmienić serwer resolver w każdej chwili.",
rrtypeDescription: "Wybierz RR-Type który chcesz monitorować",
pauseMonitorMsg: "Czy na pewno chcesz wstrzymać?",
Settings: "Ustawienia",
Dashboard: "Panel",
"New Update": "Nowa aktualizacja",
Language: "Język",
Appearance: "Wygląd",
Theme: "Motyw",
General: "Ogólne",
Version: "Wersja",
"Check Update On GitHub": "Sprawdź aktualizację na GitHub.",
List: "Lista",
Add: "Dodaj",
"Add New Monitor": "Dodaj nowy monitor",
"Quick Stats": "Szybkie statystyki",
Up: "Online",
Down: "Offline",
Pending: "Oczekujący",
Unknown: "Nieznane",
Pause: "Pauza",
Name: "Nazwa",
Status: "Status",
DateTime: "Data i godzina",
Message: "Wiadomość",
"No important events": "Brak ważnych wydarzeń",
Resume: "Wznów",
Edit: "Edytuj",
Delete: "Usuń",
Current: "aktualny",
Uptime: "Czas pracy",
"Cert Exp.": "Wygaśnięcie certyfikatu",
days: "dni",
day: "dzień",
"-day": " dni",
hour: "godzina",
"-hour": " godziny",
Response: "Odpowiedź",
Ping: "Ping",
"Monitor Type": "Typ monitora",
Keyword: "Słowo kluczowe",
"Friendly Name": "Przyjazna nazwa",
URL: "URL",
Hostname: "Nazwa hosta",
Port: "Port",
"Heartbeat Interval": "Interwał bicia serca",
Retries: "Prób",
Advanced: "Zaawansowane",
"Upside Down Mode": "Tryb do góry nogami",
"Max. Redirects": "Maks. przekierowania",
"Accepted Status Codes": "Akceptowane kody statusu",
Save: "Zapisz",
Notifications: "Powiadomienia",
"Not available, please setup.": "Niedostępne, proszę skonfigurować.",
"Setup Notification": "Konfiguracja powiadomień",
Light: "Jasny",
Dark: "Ciemny",
Auto: "Automatyczny",
"Theme - Heartbeat Bar": "Motyw - pasek bicia serca",
Normal: "Normalne",
Bottom: "Na dole",
None: "Brak",
Timezone: "Strefa czasowa",
"Search Engine Visibility": "Widoczność w wyszukiwarce",
"Allow indexing": "Pozwól na indeksowanie",
"Discourage search engines from indexing site": "Zniechęcaj wyszukiwarki do indeksowania strony",
"Change Password": "Zmień hasło",
"Current Password": "Aktualne hasło",
"New Password": "Nowe hasło",
"Repeat New Password": "Powtórz nowe hasło",
"Update Password": "Zaktualizuj hasło",
"Disable Auth": "Wyłącz autoryzację",
"Enable Auth": "Włącz autoryzację ",
Logout: "Wyloguj się",
Leave: "Zostaw",
"I understand, please disable": "Rozumiem, proszę wyłączyć",
Confirm: "Potwierdź",
Yes: "Tak",
No: "Nie",
Username: "Nazwa użytkownika",
Password: "Hasło",
"Remember me": "Zapamiętaj mnie",
Login: "Zaloguj się",
"No Monitors, please": "Brak monitorów, proszę",
"add one": "dodaj jeden",
"Notification Type": "Typ powiadomienia",
Email: "Email",
Test: "Test",
"Certificate Info": "Informacje o certyfikacie",
"Resolver Server": "Server resolver",
"Resource Record Type": "Typ rekordu zasobów",
"Last Result": "Ostatni wynik",
"Create your admin account": "Utwórz swoje konto administratora",
"Repeat Password": "Powtórz hasło",
respTime: "Czas odp. (ms)",
notAvailableShort: "N/A",
Create: "Stwórz",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Создайте аккаунт администратора",
"Repeat Password": "Повторите пароль",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Naprivi administratorski nalog",
"Repeat Password": "Ponovite lozinku",
respTime: "Vreme odg. (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "Наприви администраторски налог",
"Repeat Password": "Поновите лозинку",
respTime: "Време одг. (мс)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -1,14 +1,14 @@
export default {
languageName: "Swedish",
languageName: "Svenska",
checkEverySecond: "Uppdatera var {0} sekund.",
"Avg.": "Genomsnitt ",
"Avg.": "Genomsnittligt ",
retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",
maxRedirectDescription: "Max antal omdirigeringar att följa. Välj 0 för att avaktivera omdirigeringar.",
acceptedStatusCodesDescription: "Välj statuskoder som räknas som lyckade.",
passwordNotMatchMsg: "Det bekräftade lösenordet stämmer ej överens.",
notificationDescription: "Vänligen lägg till en notistjänst till övervakaren.",
notificationDescription: "Vänligen lägg till en notistjänst till dina övervakare.",
keywordDescription: "Sök efter nyckelord i ren HTML eller JSON-svar. Sökningen är skiftkänslig.",
pauseDashboardHome: "Pausa",
deleteMonitorMsg: "Är du säker på att du vill ta bort den här övervakningen?",
@@ -33,10 +33,10 @@ export default {
Down: "Nere",
Pending: "Pågående",
Unknown: "Okänt",
Pause: "Paus",
Pause: "Pausa",
Name: "Namn",
Status: "Status",
DateTime: "DatumTid",
DateTime: "Datum & Tid",
Message: "Meddelande",
"No important events": "Inga viktiga händelser",
Resume: "Återuppta",
@@ -44,17 +44,17 @@ export default {
Delete: "Ta bort",
Current: "Nuvarande",
Uptime: "Drifttid",
"Cert Exp.": "Certifikatsutgång",
"Cert Exp.": "Certifikat utgår",
days: "dagar",
day: "dag",
"-day": "-dag",
"-day": " dagar",
hour: "timme",
"-hour": "-timme",
"-hour": " timmar",
Response: "Svar",
Ping: "Ping",
"Monitor Type": "Övervakningstyp",
Keyword: "Nyckelord",
"Friendly Name": "Vänligt Namn",
"Friendly Name": "Namn",
URL: "URL",
Hostname: "Värdnamn",
Port: "Port",
@@ -67,14 +67,14 @@ export default {
Save: "Spara",
Notifications: "Notiser",
"Not available, please setup.": "Ej tillgänglig, vänligen konfigurera.",
"Setup Notification": "Konfigurera Notis",
"Setup Notification": "Ny Notistjänst",
Light: "Ljust",
Dark: "Mörkt",
Auto: "Automatisk",
Auto: "Automatiskt",
"Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
Normal: "Normal",
Bottom: "Botten",
None: "Ingen",
None: "Tomt",
Timezone: "Tidszon",
"Search Engine Visibility": "Synlighet på Sökmotorer",
"Allow indexing": "Tillåt indexering",
@@ -107,6 +107,25 @@ export default {
"Last Result": "Senaste resultat",
"Create your admin account": "Skapa ditt administratörskonto",
"Repeat Password": "Upprepa Lösenord",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
respTime: "Svarstid (ms)",
notAvailableShort: "Ej Tillg.",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

View File

@@ -13,7 +13,7 @@ export default {
pauseDashboardHome: "暂停",
deleteMonitorMsg: "确定要删除此监控吗?",
deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。",
resoverserverDescription: "默认服务器 Cloudflare可以修改为任意你想要使用的DNS服务器",
resoverserverDescription: "可自定义要使用的DNS服务器",
rrtypeDescription: "选择要监控的资源记录类型",
pauseMonitorMsg: "确定要暂停吗?",
Settings: "设置",
@@ -108,5 +108,24 @@ export default {
"Create your admin account": "创建管理员账号",
"Repeat Password": "重复密码",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "创建",
clearEventsMsg: "确定要删除此监控项的所有事件吗?",
clearHeartbeatsMsg: "确定要删除此监控项的所有状态吗?",
confirmClearStatisticsMsg: "确定要删除所有统计信息吗?",
"Clear Data": "清除数据",
Events: "事件",
Heartbeats: "心跳",
"Auto Get": "自动获取",
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
"Default enabled": "默认开启",
"Also apply to existing monitors": "应用到所有监控项",
"Import/Export Backup": "导入/导出备份",
Export: "导出",
Import: "导入",
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
backupDescription2: "注意: 不包括历史状态和事件数据",
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
alertNoFile: "请选择一个文件导入",
alertWrongFileType: "请选择一个 JSON 格式的文件"
}

View File

@@ -108,5 +108,24 @@ export default {
"Create your admin account": "製作你的管理員帳號",
"Repeat Password": "重複密碼",
respTime: "反應時間 (ms)",
notAvailableShort: "N/A"
notAvailableShort: "N/A",
Create: "建立",
clearEventsMsg: "是否確定刪除這個監測器的所有事件?",
clearHeartbeatsMsg: "是否確定刪除這個監測器的所有脈搏資料?",
confirmClearStatisticsMsg: "是否確定刪除所有監測器的脈搏資料?(您的監測器會繼續正常運作)",
"Clear Data": "清除資料",
Events: "事件",
Heartbeats: "脈搏",
"Auto Get": "自動獲取",
enableDefaultNotificationDescription: "新增監測器時這個通知會預設啟用,當然每個監測器亦可分別控制開關。",
"Default enabled": "預設通知",
"Also apply to existing monitors": "同時取用至目前所有監測器",
"Import/Export Backup": "匯入/匯出 備份",
Export: "匯出",
Import: "匯入",
backupDescription: "您可以備份所有監測器及所有通知。",
backupDescription2: "註:此備份不包括歷史記錄。",
backupDescription3: "此備份可能包含了一些敏感資料如通知裡的 Token請小心保存備份。",
alertNoFile: "請選擇一個檔案",
alertWrongFileType: "請選擇 JSON 檔案"
}

View File

@@ -28,7 +28,8 @@ import zhHK from "./languages/zh-HK";
import deDE from "./languages/de-DE";
import nlNL from "./languages/nl-NL";
import esEs from "./languages/es-ES";
import fr from "./languages/fr";
import frFR from "./languages/fr-FR";
import itIT from "./languages/it-IT";
import ja from "./languages/ja";
import daDK from "./languages/da-DK";
import sr from "./languages/sr";
@@ -37,6 +38,8 @@ import svSE from "./languages/sv-SE";
import koKR from "./languages/ko-KR";
import ruRU from "./languages/ru-RU";
import zhCN from "./languages/zh-CN";
import pl from "./languages/pl"
import etEE from "./languages/et-EE"
const routes = [
{
@@ -105,7 +108,8 @@ const languageList = {
"de-DE": deDE,
"nl-NL": nlNL,
"es-ES": esEs,
"fr": fr,
"fr-FR": frFR,
"it-IT": itIT,
"ja": ja,
"da-DK": daDK,
"sr": sr,
@@ -114,6 +118,8 @@ const languageList = {
"ko-KR": koKR,
"ru-RU": ruRU,
"zh-CN": zhCN,
"pl": pl,
"et-EE": etEE,
};
const i18n = createI18n({

View File

@@ -107,8 +107,8 @@ export default {
}
});
socket.on("heartbeatList", (monitorID, data) => {
if (! (monitorID in this.heartbeatList)) {
socket.on("heartbeatList", (monitorID, data, overwrite = false) => {
if (! (monitorID in this.heartbeatList) || overwrite) {
this.heartbeatList[monitorID] = data;
} else {
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
@@ -127,8 +127,8 @@ export default {
this.certInfoList[monitorID] = JSON.parse(data)
});
socket.on("importantHeartbeatList", (monitorID, data) => {
if (! (monitorID in this.importantHeartbeatList)) {
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
this.importantHeartbeatList[monitorID] = data;
} else {
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
@@ -254,6 +254,21 @@ export default {
this.importantHeartbeatList = {}
},
uploadBackup(uploadedJSON, callback) {
socket.emit("uploadBackup", uploadedJSON, callback)
},
clearEvents(monitorID, callback) {
socket.emit("clearEvents", monitorID, callback)
},
clearHeartbeats(monitorID, callback) {
socket.emit("clearHeartbeats", monitorID, callback)
},
clearStatistics(callback) {
socket.emit("clearStatistics", callback)
},
},
computed: {

View File

@@ -11,7 +11,7 @@ export default {
mounted() {
// Default Light
if (! this.userTheme) {
this.userTheme = "light";
this.userTheme = "auto";
}
// Default Heartbeat Bar

View File

@@ -38,7 +38,7 @@
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
<td>{{ beat.name }}</td>
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ beat.name }}</router-link></td>
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>

View File

@@ -133,6 +133,23 @@
</div>
<div class="shadow-box table-shadow-box">
<div class="dropdown dropdown-clear-data">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
<font-awesome-icon icon="trash" /> {{ $t("Clear Data") }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item" @click="clearEventsDialog">
{{ $t("Events") }}
</button>
</li>
<li>
<button type="button" class="dropdown-item" @click="clearHeartbeatsDialog">
{{ $t("Heartbeats") }}
</button>
</li>
</ul>
</div>
<table class="table table-borderless table-hover">
<thead>
<tr>
@@ -172,6 +189,14 @@
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMonitor">
{{ $t("deleteMonitorMsg") }}
</Confirm>
<Confirm ref="confirmClearEvents" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearEvents">
{{ $t("clearEventsMsg") }}
</Confirm>
<Confirm ref="confirmClearHeartbeats" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearHeartbeats">
{{ $t("clearHeartbeatsMsg") }}
</Confirm>
</div>
</transition>
</template>
@@ -251,6 +276,7 @@ export default {
importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id]
}
@@ -313,6 +339,14 @@ export default {
this.$refs.confirmDelete.show();
},
clearEventsDialog() {
this.$refs.confirmClearEvents.show();
},
clearHeartbeatsDialog() {
this.$refs.confirmClearHeartbeats.show();
},
deleteMonitor() {
this.$root.deleteMonitor(this.monitor.id, (res) => {
if (res.ok) {
@@ -324,6 +358,21 @@ export default {
})
},
clearEvents() {
this.$root.clearEvents(this.monitor.id, (res) => {
if (! res.ok) {
toast.error(res.msg);
}
})
},
clearHeartbeats() {
this.$root.clearHeartbeats(this.monitor.id, (res) => {
if (! res.ok) {
toast.error(res.msg);
}
})
},
},
}
</script>
@@ -340,16 +389,20 @@ export default {
@media (max-width: 550px) {
.functions {
text-align: center;
}
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
}
}
.ping-chart-wrapper {
padding: 10px !important;
}
.dropdown-clear-data {
margin-bottom: 10px;
}
}
@media (max-width: 400px) {
@@ -364,6 +417,13 @@ export default {
padding-left: 25px;
padding-right: 25px;
}
.dropdown-clear-data {
button {
display: block;
padding-top: 4px;
}
}
}
.url {
@@ -417,10 +477,30 @@ table {
color: black;
}
.dropdown-clear-data {
float: right;
}
.dark {
.keyword {
color: $dark-font-color;
}
.dropdown-clear-data {
ul {
background-color: $dark-bg;
border-color: $dark-bg2;
border-width: 2px;
li button {
color: $dark-font-color;
}
li button:hover {
background-color: $dark-bg2;
}
}
}
}
</style>

View File

@@ -10,7 +10,7 @@
<div class="my-3">
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<select id="type" v-model="monitor.type" class="form-select">
<option value="http">
HTTP(s)
</option>
@@ -178,6 +178,8 @@
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="notification.isDefault == true" class="badge bg-primary ms-2">Default</span>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
@@ -188,7 +190,7 @@
</div>
</form>
<NotificationDialog ref="notificationDialog" />
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
</div>
</transition>
</template>
@@ -281,7 +283,7 @@ export default {
methods: {
init() {
if (this.isAdd) {
console.log("??????")
this.monitor = {
type: "http",
name: "",
@@ -296,6 +298,12 @@ export default {
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
}
for (let i = 0; i < this.$root.notificationList.length; i++) {
if (this.$root.notificationList[i].isDefault == true) {
this.monitor.notificationIDList[this.$root.notificationList[i].id] = true;
}
}
} else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
if (res.ok) {
@@ -330,6 +338,12 @@ export default {
})
}
},
// Added a Notification Event
// Enable it if the notification is added in EditMonitor.vue
addedNotification(id) {
this.monitor.notificationIDList[id] = true;
},
},
}
</script>

View File

@@ -120,12 +120,34 @@
</form>
</template>
<h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
<p>
{{ $t("backupDescription") }} <br />
({{ $t("backupDescription2") }}) <br />
</p>
<div class="input-group mb-3">
<button class="btn btn-outline-primary" @click="downloadBackup">{{ $t("Export") }}</button>
<button type="button" class="btn btn-outline-primary" :disabled="processing" @click="importBackup">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Import") }}
</button>
<input id="importBackup" type="file" class="form-control" accept="application/json">
</div>
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
{{ importAlert }}
</div>
<p><strong>{{ $t("backupDescription3") }}</strong></p>
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="mb-3">
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
<button class="btn btn-outline-danger me-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
</div>
</template>
</div>
@@ -166,53 +188,76 @@
<NotificationDialog ref="notificationDialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<template v-if="$i18n.locale === 'en' ">
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</template>
<template v-if="$i18n.locale === 'es-ES' ">
<p>Seguro que deseas <strong>deshabilitar la autenticación</strong>?</p>
<p>Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.</p>
<p>Por favor usar con cuidado.</p>
</template>
<template v-if="$i18n.locale === 'zh-HK' ">
<template v-else-if="$i18n.locale === 'zh-HK' ">
<p>你是否確認<strong>取消登入認証</strong></p>
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家例如 Cloudflare Access</p>
<p>請小心使用</p>
</template>
<template v-if="$i18n.locale === 'zh-CN' ">
<template v-else-if="$i18n.locale === 'zh-CN' ">
<p>是否确定 <strong>取消登录验证</strong></p>
<p>这是为 <strong>有第三方认证</strong> 的用户提供的功能 Cloudflare Access</p>
<p>请谨慎使用</p>
</template>
<template v-if="$i18n.locale === 'de-DE' ">
<template v-else-if="$i18n.locale === 'de-DE' ">
<p>Bist du sicher das du die <strong>Authentifizierung deaktivieren</strong> möchtest?</p>
<p>Es ist für <strong>jemanden der eine externe Authentifizierung</strong> vor Uptime Kuma geschaltet hat, wie z.B. Cloudflare Access.</p>
<p>Bitte mit Vorsicht nutzen.</p>
</template>
<template v-if="$i18n.locale === 'sr' ">
<template v-else-if="$i18n.locale === 'sr' ">
<p>Да ли сте сигурни да желите да <strong>искључите аутентификацију</strong>?</p>
<p>То је за <strong>оне који имају додату аутентификацију</strong> испред Uptime Kuma као на пример Cloudflare Access.</p>
<p>Молим Вас користите ово са пажњом.</p>
</template>
<template v-if="$i18n.locale === 'sr-latn' ">
<template v-else-if="$i18n.locale === 'sr-latn' ">
<p>Da li ste sigurni da želite da <strong>isključite autentifikaciju</strong>?</p>
<p>To je za <strong>one koji imaju dodatu autentifikaciju</strong> ispred Uptime Kuma kao na primer Cloudflare Access.</p>
<p>Molim Vas koristite ovo sa pažnjom.</p>
</template>
<template v-if="$i18n.locale === 'ko-KR' ">
<template v-else-if="$i18n.locale === 'ko-KR' ">
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
<p> 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong> Uptime Kuma 앞에 사용자를 위한 기능이에요.</p>
<p>신중하게 사용하세요.</p>
</template>
<template v-else-if="$i18n.locale === 'pl' ">
<p>Czy na pewno chcesz <strong>wyłączyć autoryzację</strong>?</p>
<p>Jest przeznaczony dla <strong>kogoś, kto ma autoryzację zewnętrzną</strong> przed Uptime Kuma, taką jak Cloudflare Access.</p>
<p>Proszę używać ostrożnie.</p>
</template>
<template v-else-if="$i18n.locale === 'et-EE' ">
<p>Kas soovid <strong>lülitada autentimise välja</strong>?</p>
<p>Kastuamiseks <strong>välise autentimispakkujaga</strong>, näiteks Cloudflare Access.</p>
<p>Palun kasuta vastutustundlikult.</p>
</template>
<template v-else-if="$i18n.locale === 'it-IT' ">
<p>Si è certi di voler <strong>disabilitare l'autenticazione</strong>?</p>
<p>È per <strong>chi ha l'autenticazione gestita da terze parti</strong> messa davanti ad Uptime Kuma, ad esempio Cloudflare Access.</p>
<p>Utilizzare con attenzione.</p>
</template>
<!-- English (en) -->
<template v-else>
<p>Are you sure want to <strong>disable auth</strong>?</p>
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
<p>Please use it carefully.</p>
</template>
</Confirm>
<Confirm ref="confirmClearStatistics" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearStatistics">
{{ $t("confirmClearStatisticsMsg") }}
</Confirm>
</div>
</transition>
@@ -251,6 +296,8 @@ export default {
},
loaded: false,
importAlert: null,
processing: false,
}
},
watch: {
@@ -312,6 +359,10 @@ export default {
this.$refs.confirmDisableAuth.show();
},
confirmClearStatistics() {
this.$refs.confirmClearStatistics.show();
},
disableAuth() {
this.settings.disableAuth = true;
this.saveSettings();
@@ -323,6 +374,61 @@ export default {
this.$root.storage().removeItem("token");
},
downloadBackup() {
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
let fileName = `Uptime_Kuma_Backup_${time}.json`;
let monitorList = Object.values(this.$root.monitorList);
let exportData = {
version: this.$root.info.version,
notificationList: this.$root.notificationList,
monitorList: monitorList,
}
exportData = JSON.stringify(exportData);
let downloadItem = document.createElement("a");
downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData));
downloadItem.setAttribute("download", fileName);
downloadItem.click();
},
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("importBackup").files;
if (uploadItem.length <= 0) {
this.processing = false;
return this.importAlert = this.$t("alertNoFile")
}
if (uploadItem.item(0).type !== "application/json") {
this.processing = false;
return this.importAlert = this.$t("alertWrongFileType")
}
let fileReader = new FileReader();
fileReader.readAsText(uploadItem.item(0));
fileReader.onload = item => {
this.$root.uploadBackup(item.target.result, (res) => {
this.processing = false;
if (res.ok) {
toast.success(res.msg);
} else {
toast.error(res.msg);
}
})
}
},
clearStatistics() {
this.$root.clearStatistics((res) => {
if (res.ok) {
this.$router.go();
} else {
toast.error(res.msg);
}
})
},
},
}
</script>
@@ -351,6 +457,18 @@ export default {
.btn-check:hover + .btn-outline-primary {
color: #000;
}
#importBackup {
&::file-selector-button {
color: $primary;
background-color: $dark-bg;
}
&:hover:not(:disabled):not([readonly])::file-selector-button {
color: $dark-font-color2;
background-color: $primary;
}
}
}
footer {

View File

@@ -14,6 +14,15 @@
</p>
<div class="form-floating">
<select id="language" v-model="$i18n.locale" class="form-select">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
{{ $i18n.messages[lang].languageName }}
</option>
</select>
<label for="language" class="form-label">{{ $t("Language") }}</label>
</div>
<div class="form-floating mt-3">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
@@ -29,7 +38,7 @@
</div>
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
Create
{{ $t("Create") }}
</button>
</form>
</div>
@@ -49,6 +58,11 @@ export default {
repeatPassword: "",
}
},
watch: {
"$i18n.locale"() {
localStorage.locale = this.$i18n.locale;
},
},
mounted() {
this.$root.getSocket().emit("needSetup", (needSetup) => {
if (! needSetup) {
@@ -71,7 +85,12 @@ export default {
this.$root.toastRes(res)
if (res.ok) {
this.$router.push("/")
this.processing = true;
this.$root.login(this.username, this.password, (res) => {
this.processing = false;
this.$router.push("/")
})
}
})
},