LouisLam
3 years ago
137 changed files with 22175 additions and 8724 deletions
@ -1,11 +0,0 @@ |
|||
spec: |
|||
name: uptime-kuma |
|||
services: |
|||
- name: server |
|||
git: |
|||
repo_clone_url: https://github.com/louislam/uptime-kuma |
|||
branch: master |
|||
http_port: 3001 |
|||
build_command: npm run setup |
|||
run_command: npm run start-server |
|||
|
@ -0,0 +1,12 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] |
|||
#patreon: # Replace with a single Patreon username |
|||
open_collective: uptime-kuma # Replace with a single Open Collective username |
|||
#ko_fi: # Replace with a single Ko-fi username |
|||
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel |
|||
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry |
|||
#liberapay: # Replace with a single Liberapay username |
|||
#issuehunt: # Replace with a single IssueHunt username |
|||
#otechie: # Replace with a single Otechie username |
|||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] |
@ -1,71 +0,0 @@ |
|||
# For most projects, this workflow file will not need changing; you simply need |
|||
# to commit it to your repository. |
|||
# |
|||
# You may wish to alter this file to override the set of languages analyzed, |
|||
# or to provide custom queries or build logic. |
|||
# |
|||
# ******** NOTE ******** |
|||
# We have attempted to detect the languages in your repository. Please check |
|||
# the `language` matrix defined below to confirm you have the correct set of |
|||
# supported CodeQL languages. |
|||
# |
|||
name: "CodeQL" |
|||
|
|||
on: |
|||
push: |
|||
branches: [ master ] |
|||
pull_request: |
|||
# The branches below must be a subset of the branches above |
|||
branches: [ master ] |
|||
schedule: |
|||
- cron: '35 5 * * 2' |
|||
|
|||
jobs: |
|||
analyze: |
|||
name: Analyze |
|||
runs-on: ubuntu-latest |
|||
permissions: |
|||
actions: read |
|||
contents: read |
|||
security-events: write |
|||
|
|||
strategy: |
|||
fail-fast: false |
|||
matrix: |
|||
language: [ 'javascript' ] |
|||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] |
|||
# Learn more: |
|||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed |
|||
|
|||
steps: |
|||
- name: Checkout repository |
|||
uses: actions/checkout@v2 |
|||
|
|||
# Initializes the CodeQL tools for scanning. |
|||
- name: Initialize CodeQL |
|||
uses: github/codeql-action/init@v1 |
|||
with: |
|||
languages: ${{ matrix.language }} |
|||
# If you wish to specify custom queries, you can do so here or in a config file. |
|||
# By default, queries listed here will override any specified in a config file. |
|||
# Prefix the list here with "+" to use these queries and those in the config file. |
|||
# queries: ./path/to/local/query, your-org/your-repo/queries@main |
|||
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). |
|||
# If this step fails, then you should remove it and run the build manually (see below) |
|||
- name: Autobuild |
|||
uses: github/codeql-action/autobuild@v1 |
|||
|
|||
# ℹ️ Command-line programs to run using the OS shell. |
|||
# 📚 https://git.io/JvXDl |
|||
|
|||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines |
|||
# and modify them (or add more) to build your code if your project |
|||
# uses a compiled language |
|||
|
|||
#- run: | |
|||
# make bootstrap |
|||
# make release |
|||
|
|||
- name: Perform CodeQL Analysis |
|||
uses: github/codeql-action/analyze@v1 |
@ -1,3 +1,9 @@ |
|||
{ |
|||
"extends": "stylelint-config-recommended", |
|||
"extends": "stylelint-config-standard", |
|||
"rules": { |
|||
"indentation": 4, |
|||
"no-descending-specificity": null, |
|||
"selector-list-comma-newline-after": null, |
|||
"declaration-empty-line-before": null |
|||
} |
|||
} |
|||
|
@ -0,0 +1 @@ |
|||
git.kuma.pet |
@ -0,0 +1,128 @@ |
|||
# Contributor Covenant Code of Conduct |
|||
|
|||
## Our Pledge |
|||
|
|||
We as members, contributors, and leaders pledge to make participation in our |
|||
community a harassment-free experience for everyone, regardless of age, body |
|||
size, visible or invisible disability, ethnicity, sex characteristics, gender |
|||
identity and expression, level of experience, education, socio-economic status, |
|||
nationality, personal appearance, race, religion, or sexual identity |
|||
and orientation. |
|||
|
|||
We pledge to act and interact in ways that contribute to an open, welcoming, |
|||
diverse, inclusive, and healthy community. |
|||
|
|||
## Our Standards |
|||
|
|||
Examples of behavior that contributes to a positive environment for our |
|||
community include: |
|||
|
|||
* Demonstrating empathy and kindness toward other people |
|||
* Being respectful of differing opinions, viewpoints, and experiences |
|||
* Giving and gracefully accepting constructive feedback |
|||
* Accepting responsibility and apologizing to those affected by our mistakes, |
|||
and learning from the experience |
|||
* Focusing on what is best not just for us as individuals, but for the |
|||
overall community |
|||
|
|||
Examples of unacceptable behavior include: |
|||
|
|||
* The use of sexualized language or imagery, and sexual attention or |
|||
advances of any kind |
|||
* Trolling, insulting or derogatory comments, and personal or political attacks |
|||
* Public or private harassment |
|||
* Publishing others' private information, such as a physical or email |
|||
address, without their explicit permission |
|||
* Other conduct which could reasonably be considered inappropriate in a |
|||
professional setting |
|||
|
|||
## Enforcement Responsibilities |
|||
|
|||
Community leaders are responsible for clarifying and enforcing our standards of |
|||
acceptable behavior and will take appropriate and fair corrective action in |
|||
response to any behavior that they deem inappropriate, threatening, offensive, |
|||
or harmful. |
|||
|
|||
Community leaders have the right and responsibility to remove, edit, or reject |
|||
comments, commits, code, wiki edits, issues, and other contributions that are |
|||
not aligned to this Code of Conduct, and will communicate reasons for moderation |
|||
decisions when appropriate. |
|||
|
|||
## Scope |
|||
|
|||
This Code of Conduct applies within all community spaces, and also applies when |
|||
an individual is officially representing the community in public spaces. |
|||
Examples of representing our community include using an official e-mail address, |
|||
posting via an official social media account, or acting as an appointed |
|||
representative at an online or offline event. |
|||
|
|||
## Enforcement |
|||
|
|||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
|||
reported to the community leaders responsible for enforcement at |
|||
louis@uptimekuma.louislam.net. |
|||
All complaints will be reviewed and investigated promptly and fairly. |
|||
|
|||
All community leaders are obligated to respect the privacy and security of the |
|||
reporter of any incident. |
|||
|
|||
## Enforcement Guidelines |
|||
|
|||
Community leaders will follow these Community Impact Guidelines in determining |
|||
the consequences for any action they deem in violation of this Code of Conduct: |
|||
|
|||
### 1. Correction |
|||
|
|||
**Community Impact**: Use of inappropriate language or other behavior deemed |
|||
unprofessional or unwelcome in the community. |
|||
|
|||
**Consequence**: A private, written warning from community leaders, providing |
|||
clarity around the nature of the violation and an explanation of why the |
|||
behavior was inappropriate. A public apology may be requested. |
|||
|
|||
### 2. Warning |
|||
|
|||
**Community Impact**: A violation through a single incident or series |
|||
of actions. |
|||
|
|||
**Consequence**: A warning with consequences for continued behavior. No |
|||
interaction with the people involved, including unsolicited interaction with |
|||
those enforcing the Code of Conduct, for a specified period of time. This |
|||
includes avoiding interactions in community spaces as well as external channels |
|||
like social media. Violating these terms may lead to a temporary or |
|||
permanent ban. |
|||
|
|||
### 3. Temporary Ban |
|||
|
|||
**Community Impact**: A serious violation of community standards, including |
|||
sustained inappropriate behavior. |
|||
|
|||
**Consequence**: A temporary ban from any sort of interaction or public |
|||
communication with the community for a specified period of time. No public or |
|||
private interaction with the people involved, including unsolicited interaction |
|||
with those enforcing the Code of Conduct, is allowed during this period. |
|||
Violating these terms may lead to a permanent ban. |
|||
|
|||
### 4. Permanent Ban |
|||
|
|||
**Community Impact**: Demonstrating a pattern of violation of community |
|||
standards, including sustained inappropriate behavior, harassment of an |
|||
individual, or aggression toward or disparagement of classes of individuals. |
|||
|
|||
**Consequence**: A permanent ban from any sort of public interaction within |
|||
the community. |
|||
|
|||
## Attribution |
|||
|
|||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], |
|||
version 2.0, available at |
|||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. |
|||
|
|||
Community Impact Guidelines were inspired by [Mozilla's code of conduct |
|||
enforcement ladder](https://github.com/mozilla/diversity). |
|||
|
|||
[homepage]: https://www.contributor-covenant.org |
|||
|
|||
For answers to common questions about this code of conduct, see the FAQ at |
|||
https://www.contributor-covenant.org/faq. Translations are available at |
|||
https://www.contributor-covenant.org/translations. |
@ -0,0 +1,152 @@ |
|||
# Project Info |
|||
|
|||
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that. |
|||
|
|||
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json. |
|||
|
|||
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working. |
|||
|
|||
# Can I create a pull request for Uptime Kuma? |
|||
|
|||
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested. |
|||
|
|||
If you are not sure, feel free to create an empty pull request draft first. |
|||
|
|||
## Pull Request Examples |
|||
|
|||
### ✅ High - Medium Priority |
|||
|
|||
- Add a new notification |
|||
- Add a chart |
|||
- Fix a bug |
|||
|
|||
### *️⃣ Requires one more reviewer |
|||
|
|||
I do not have such knowledge to test it. |
|||
|
|||
- Add k8s supports |
|||
|
|||
### *️⃣ Low Priority |
|||
|
|||
It changed my current workflow and require further studies. |
|||
|
|||
- Change my release approach |
|||
|
|||
### ❌ Won't Merge |
|||
|
|||
- Duplicated pull request |
|||
- Buggy |
|||
- Existing logic is completely modified or deleted |
|||
- A function that is completely out of scope |
|||
|
|||
# Project Styles |
|||
|
|||
I personally do not like something need to learn so much and need to config so much before you can finally start the app. |
|||
|
|||
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so: |
|||
|
|||
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run |
|||
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go |
|||
- All settings in frontend. |
|||
- Easy to use |
|||
|
|||
# Coding Styles |
|||
|
|||
- Follow .editorconfig |
|||
- Follow eslint |
|||
|
|||
## Name convention |
|||
|
|||
- Javascript/Typescript: camelCaseType |
|||
- SQLite: underscore_type |
|||
- CSS/SCSS: dash-type |
|||
|
|||
# Tools |
|||
- Node.js >= 14 |
|||
- Git |
|||
- IDE that supports .editorconfig and eslint (I am using Intellji Idea) |
|||
- A SQLite tool (I am using SQLite Expert Personal) |
|||
|
|||
# Install dependencies |
|||
|
|||
```bash |
|||
npm install --dev |
|||
``` |
|||
|
|||
For npm@7, you need --legacy-peer-deps |
|||
|
|||
``` |
|||
npm install --legacy-peer-deps --dev |
|||
``` |
|||
|
|||
# Backend Dev |
|||
|
|||
```bash |
|||
npm run start-server |
|||
|
|||
# Or |
|||
|
|||
node server/server.js |
|||
``` |
|||
|
|||
It binds to 0.0.0.0:3001 by default. |
|||
|
|||
|
|||
## Backend Details |
|||
|
|||
It is mainly a socket.io app + express.js. |
|||
|
|||
express.js is just used for serving the frontend built files (index.html, .js and .css etc.) |
|||
|
|||
# Frontend Dev |
|||
|
|||
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000. |
|||
|
|||
```bash |
|||
npm run dev |
|||
``` |
|||
|
|||
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix. |
|||
|
|||
You can use Vue Devtool Chrome extension for debugging. |
|||
|
|||
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh: |
|||
|
|||
```javascript |
|||
localStorage.dev = "dev"; |
|||
``` |
|||
|
|||
So that the frontend will try to connect websocket server in 3001. |
|||
|
|||
Alternately, you can specific NODE_ENV to "development". |
|||
|
|||
|
|||
## Build the frontend |
|||
|
|||
```bash |
|||
npm run build |
|||
``` |
|||
|
|||
## Frontend Details |
|||
|
|||
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router. |
|||
|
|||
The router in "src/main.js" |
|||
|
|||
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages. |
|||
|
|||
The data and socket logic in "src/mixins/socket.js" |
|||
|
|||
# Database Migration |
|||
|
|||
1. create `patch{num}.sql` in `./db/` |
|||
1. update `latestVersion` in `./server/database.js` |
|||
|
|||
# Unit Test |
|||
|
|||
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points. |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,14 @@ |
|||
# Security Policy |
|||
|
|||
## Supported Versions |
|||
|
|||
Use this section to tell people about which versions of your project are |
|||
currently being supported with security updates. |
|||
|
|||
| Version | Supported | |
|||
| ------- | ------------------ | |
|||
| 1.x.x | :white_check_mark: | |
|||
|
|||
## Reporting a Vulnerability |
|||
|
|||
https://github.com/louislam/uptime-kuma/issues |
@ -1,7 +0,0 @@ |
|||
{ |
|||
"name": "Uptime Kuma", |
|||
"description": "A fancy self-hosted monitoring tool", |
|||
"repository": "https://github.com/louislam/uptime-kuma", |
|||
"logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", |
|||
"keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"] |
|||
} |
Binary file not shown.
@ -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; |
@ -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; |
@ -0,0 +1,70 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
PRAGMA foreign_keys = off; |
|||
|
|||
BEGIN TRANSACTION; |
|||
|
|||
create table monitor_dg_tmp ( |
|||
id INTEGER not null primary key autoincrement, |
|||
name VARCHAR(150), |
|||
active BOOLEAN default 1 not null, |
|||
user_id INTEGER references user on update cascade on delete |
|||
set |
|||
null, |
|||
interval INTEGER default 20 not null, |
|||
url TEXT, |
|||
type VARCHAR(20), |
|||
weight INTEGER default 2000, |
|||
hostname VARCHAR(255), |
|||
port INTEGER, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
keyword VARCHAR(255), |
|||
maxretries INTEGER NOT NULL DEFAULT 0, |
|||
ignore_tls BOOLEAN default 0 not null, |
|||
upside_down BOOLEAN default 0 not null |
|||
); |
|||
|
|||
insert into |
|||
monitor_dg_tmp( |
|||
id, |
|||
name, |
|||
active, |
|||
user_id, |
|||
interval, |
|||
url, |
|||
type, |
|||
weight, |
|||
hostname, |
|||
port, |
|||
keyword, |
|||
maxretries, |
|||
ignore_tls, |
|||
upside_down |
|||
) |
|||
select |
|||
id, |
|||
name, |
|||
active, |
|||
user_id, |
|||
interval, |
|||
url, |
|||
type, |
|||
weight, |
|||
hostname, |
|||
port, |
|||
keyword, |
|||
maxretries, |
|||
ignore_tls, |
|||
upside_down |
|||
from |
|||
monitor; |
|||
|
|||
drop table monitor; |
|||
|
|||
alter table |
|||
monitor_dg_tmp rename to monitor; |
|||
|
|||
create index user_id on monitor (user_id); |
|||
|
|||
COMMIT; |
|||
|
|||
PRAGMA foreign_keys = on; |
@ -0,0 +1,74 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
PRAGMA foreign_keys = off; |
|||
|
|||
BEGIN TRANSACTION; |
|||
|
|||
create table monitor_dg_tmp ( |
|||
id INTEGER not null primary key autoincrement, |
|||
name VARCHAR(150), |
|||
active BOOLEAN default 1 not null, |
|||
user_id INTEGER references user on update cascade on delete |
|||
set |
|||
null, |
|||
interval INTEGER default 20 not null, |
|||
url TEXT, |
|||
type VARCHAR(20), |
|||
weight INTEGER default 2000, |
|||
hostname VARCHAR(255), |
|||
port INTEGER, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
keyword VARCHAR(255), |
|||
maxretries INTEGER NOT NULL DEFAULT 0, |
|||
ignore_tls BOOLEAN default 0 not null, |
|||
upside_down BOOLEAN default 0 not null, |
|||
maxredirects INTEGER default 10 not null, |
|||
accepted_statuscodes_json TEXT default '["200-299"]' not null |
|||
); |
|||
|
|||
insert into |
|||
monitor_dg_tmp( |
|||
id, |
|||
name, |
|||
active, |
|||
user_id, |
|||
interval, |
|||
url, |
|||
type, |
|||
weight, |
|||
hostname, |
|||
port, |
|||
created_date, |
|||
keyword, |
|||
maxretries, |
|||
ignore_tls, |
|||
upside_down |
|||
) |
|||
select |
|||
id, |
|||
name, |
|||
active, |
|||
user_id, |
|||
interval, |
|||
url, |
|||
type, |
|||
weight, |
|||
hostname, |
|||
port, |
|||
created_date, |
|||
keyword, |
|||
maxretries, |
|||
ignore_tls, |
|||
upside_down |
|||
from |
|||
monitor; |
|||
|
|||
drop table monitor; |
|||
|
|||
alter table |
|||
monitor_dg_tmp rename to monitor; |
|||
|
|||
create index user_id on monitor (user_id); |
|||
|
|||
COMMIT; |
|||
|
|||
PRAGMA foreign_keys = on; |
@ -0,0 +1,10 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_resolve_type VARCHAR(5); |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_resolve_server VARCHAR(255); |
|||
|
|||
COMMIT; |
@ -0,0 +1,7 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_last_result VARCHAR(255); |
|||
|
|||
COMMIT; |
@ -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; |
@ -1,42 +1,37 @@ |
|||
# DON'T UPDATE TO alpine3.13, 1.14, see #41. |
|||
FROM node:14-alpine3.12 AS release |
|||
# DON'T UPDATE TO node:14-bullseye-slim, see #372. |
|||
FROM node:14-buster-slim AS build |
|||
WORKDIR /app |
|||
|
|||
# split the sqlite install here, so that it can caches the arm prebuilt |
|||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ |
|||
# do not modify it, since we don't want to re-compile the arm prebuilt again |
|||
RUN apt update && \ |
|||
apt --yes install python3 python3-pip python3-dev git g++ make && \ |
|||
ln -s /usr/bin/python3 /usr/bin/python && \ |
|||
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \ |
|||
apk del .build-deps |
|||
npm install mapbox/node-sqlite3#593c9d --build-from-source |
|||
|
|||
# Touching above code may causes sqlite3 re-compile again, painful slow. |
|||
COPY . . |
|||
RUN npm install --legacy-peer-deps && npm run build && npm prune --production |
|||
|
|||
# Install apprise |
|||
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/ |
|||
# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev |
|||
# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo |
|||
# Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev |
|||
# Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev |
|||
# Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev |
|||
# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six |
|||
# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six |
|||
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 |
|||
RUN apk add --no-cache python3 py3-pip py3-six cargo |
|||
RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev python3-dev && \ |
|||
pip3 install apprise && \ |
|||
pip3 cache purge && \ |
|||
rm -rf /root/.cache && \ |
|||
apk del .build-deps |
|||
RUN apprise --version |
|||
FROM node:14-bullseye-slim AS release |
|||
WORKDIR /app |
|||
|
|||
# New things add here |
|||
# Install Apprise, |
|||
# add sqlite3 cli for debugging in the future |
|||
# iputils-ping for ping |
|||
RUN apt update && \ |
|||
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ |
|||
sqlite3 \ |
|||
iputils-ping && \ |
|||
pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /var/lib/apt/lists/* |
|||
|
|||
COPY . . |
|||
RUN npm install && npm run build && npm prune |
|||
# Copy app files from build layer |
|||
COPY --from=build /app /app |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js |
|||
CMD ["npm", "run", "start-server"] |
|||
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 |
|||
|
@ -0,0 +1,33 @@ |
|||
# DON'T UPDATE TO alpine3.13, 1.14, see #41. |
|||
FROM node:14-alpine3.12 AS build |
|||
WORKDIR /app |
|||
|
|||
# split the sqlite install here, so that it can caches the arm prebuilt |
|||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \ |
|||
ln -s /usr/bin/python3 /usr/bin/python && \ |
|||
npm install mapbox/node-sqlite3#593c9d && \ |
|||
apk del .build-deps && \ |
|||
rm -f /usr/bin/python |
|||
|
|||
COPY . . |
|||
RUN npm install --legacy-peer-deps && npm run build && npm prune --production |
|||
|
|||
|
|||
FROM node:14-alpine3.12 AS release |
|||
WORKDIR /app |
|||
|
|||
# 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 && \ |
|||
pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /root/.cache |
|||
|
|||
# Copy app files from build layer |
|||
COPY --from=build /app /app |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js |
|||
CMD ["node", "server/server.js"] |
|||
|
|||
FROM release AS nightly |
|||
RUN npm run mark-as-nightly |
@ -0,0 +1,2 @@ |
|||
# Must enable File Sharing in Docker Desktop |
|||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh |
@ -1,19 +1,34 @@ |
|||
var http = require("http"); |
|||
var options = { |
|||
host: "localhost", |
|||
port: "3001", |
|||
timeout: 2000, |
|||
/* |
|||
* This script should be run after a period of time (180s), because the server may need some time to prepare. |
|||
*/ |
|||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; |
|||
|
|||
let client; |
|||
|
|||
if (process.env.SSL_KEY && process.env.SSL_CERT) { |
|||
client = require("https"); |
|||
} else { |
|||
client = require("http"); |
|||
} |
|||
|
|||
let options = { |
|||
host: process.env.HOST || "127.0.0.1", |
|||
port: parseInt(process.env.PORT) || 3001, |
|||
timeout: 28 * 1000, |
|||
}; |
|||
var request = http.request(options, (res) => { |
|||
console.log(`STATUS: ${res.statusCode}`); |
|||
if (res.statusCode == 200) { |
|||
process.exit(0); |
|||
} else { |
|||
process.exit(1); |
|||
} |
|||
|
|||
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"); |
|||
process.exit(1); |
|||
console.error("Health Check ERROR"); |
|||
process.exit(1); |
|||
}); |
|||
|
|||
request.end(); |
|||
|
@ -0,0 +1,245 @@ |
|||
// install.sh is generated by ./extra/install.batsh, do not modify it directly. |
|||
// "npm run compile-install-script" to compile install.sh |
|||
// The command is working on Windows PowerShell and Docker for Windows only. |
|||
|
|||
|
|||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh |
|||
println("====================="); |
|||
println("Uptime Kuma Installer"); |
|||
println("====================="); |
|||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"); |
|||
println("---------------------------------------"); |
|||
println("This script is designed for Linux and basic usage."); |
|||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"); |
|||
println("---------------------------------------"); |
|||
println(""); |
|||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"); |
|||
println("Docker - Install Uptime Kuma Docker container"); |
|||
println(""); |
|||
|
|||
if ("$1" != "") { |
|||
type = "$1"; |
|||
} else { |
|||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type"); |
|||
} |
|||
|
|||
defaultPort = "3001"; |
|||
|
|||
function checkNode() { |
|||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')"); |
|||
println("Node Version: " ++ nodeVersion); |
|||
|
|||
if (nodeVersion < "12") { |
|||
println("Error: Required Node.js 14"); |
|||
call("exit", "1"); |
|||
} |
|||
|
|||
if (nodeVersion == "12") { |
|||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested."); |
|||
} |
|||
} |
|||
|
|||
function deb() { |
|||
bash("nodeCheck=$(node -v)"); |
|||
bash("apt --yes update"); |
|||
|
|||
if (nodeCheck != "") { |
|||
checkNode(); |
|||
} else { |
|||
|
|||
// Old nodejs binary name is "nodejs" |
|||
bash("check=$(nodejs --version)"); |
|||
if (check != "") { |
|||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."); |
|||
bash("exit 1"); |
|||
} |
|||
|
|||
bash("curlCheck=$(curl --version)"); |
|||
if (curlCheck == "") { |
|||
println("Installing Curl"); |
|||
bash("apt --yes install curl"); |
|||
} |
|||
|
|||
println("Installing Node.js 14"); |
|||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt"); |
|||
bash("apt --yes install nodejs"); |
|||
bash("node -v"); |
|||
|
|||
bash("nodeCheckAgain=$(node -v)"); |
|||
|
|||
if (nodeCheckAgain == "") { |
|||
println("Error during Node.js installation"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
println("Installing Git"); |
|||
bash("apt --yes install git"); |
|||
} |
|||
} |
|||
|
|||
if (type == "local") { |
|||
defaultInstallPath = "/opt/uptime-kuma"; |
|||
|
|||
if (exists("/etc/redhat-release")) { |
|||
os = call("cat", "/etc/redhat-release"); |
|||
distribution = "rhel"; |
|||
|
|||
} else if (exists("/etc/issue")) { |
|||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')"); |
|||
if (os == "Ubuntu") { |
|||
distribution = "ubuntu"; |
|||
} |
|||
if (os == "Debian") { |
|||
distribution = "debian"; |
|||
} |
|||
} |
|||
|
|||
bash("arch=$(uname -i)"); |
|||
|
|||
println("Your OS: " ++ os); |
|||
println("Distribution: " ++ distribution); |
|||
println("Arch: " ++ arch); |
|||
|
|||
if ("$3" != "") { |
|||
port = "$3"; |
|||
} else { |
|||
call("read", "-p", "Listening Port [$defaultPort]: ", "port"); |
|||
|
|||
if (port == "") { |
|||
port = defaultPort; |
|||
} |
|||
} |
|||
|
|||
if ("$2" != "") { |
|||
installPath = "$2"; |
|||
} else { |
|||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath"); |
|||
|
|||
if (installPath == "") { |
|||
installPath = defaultInstallPath; |
|||
} |
|||
} |
|||
|
|||
// CentOS |
|||
if (distribution == "rhel") { |
|||
bash("nodeCheck=$(node -v)"); |
|||
|
|||
if (nodeCheck != "") { |
|||
checkNode(); |
|||
} else { |
|||
|
|||
bash("curlCheck=$(curl --version)"); |
|||
if (curlCheck == "") { |
|||
println("Installing Curl"); |
|||
bash("yum -y -q install curl"); |
|||
} |
|||
|
|||
println("Installing Node.js 14"); |
|||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt"); |
|||
bash("yum install -y -q nodejs"); |
|||
bash("node -v"); |
|||
|
|||
bash("nodeCheckAgain=$(node -v)"); |
|||
|
|||
if (nodeCheckAgain == "") { |
|||
println("Error during Node.js installation"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
println("Installing Git"); |
|||
bash("yum -y -q install git"); |
|||
} |
|||
|
|||
// Ubuntu |
|||
} else if (distribution == "ubuntu") { |
|||
deb(); |
|||
|
|||
// Debian |
|||
} else if (distribution == "debian") { |
|||
deb(); |
|||
|
|||
} else { |
|||
// Unknown distribution |
|||
error = 0; |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
error = 1; |
|||
println("Error: git is missing"); |
|||
} |
|||
|
|||
bash("check=$(node -v)"); |
|||
if (check == "") { |
|||
error = 1; |
|||
println("Error: node is missing"); |
|||
} |
|||
|
|||
if (error > 0) { |
|||
println("Please install above missing software"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(pm2 --version)"); |
|||
if (check == "") { |
|||
println("Installing PM2"); |
|||
bash("npm install pm2 -g"); |
|||
bash("pm2 startup"); |
|||
} |
|||
|
|||
bash("mkdir -p $installPath"); |
|||
bash("cd $installPath"); |
|||
bash("git clone https://github.com/louislam/uptime-kuma.git ."); |
|||
bash("npm run setup"); |
|||
|
|||
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port"); |
|||
|
|||
} else { |
|||
defaultVolume = "uptime-kuma"; |
|||
|
|||
bash("check=$(docker -v)"); |
|||
if (check == "") { |
|||
println("Error: docker is not found!"); |
|||
bash("exit 1"); |
|||
} |
|||
|
|||
bash("check=$(docker info)"); |
|||
|
|||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then |
|||
\"echo\" \"Error: docker is not running\" |
|||
\"exit\" \"1\" |
|||
fi"); |
|||
|
|||
if ("$3" != "") { |
|||
port = "$3"; |
|||
} else { |
|||
call("read", "-p", "Expose Port [$defaultPort]: ", "port"); |
|||
|
|||
if (port == "") { |
|||
port = defaultPort; |
|||
} |
|||
} |
|||
|
|||
if ("$2" != "") { |
|||
volume = "$2"; |
|||
} else { |
|||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume"); |
|||
|
|||
if (volume == "") { |
|||
volume = defaultVolume; |
|||
} |
|||
} |
|||
|
|||
println("Port: $port"); |
|||
println("Volume: $volume"); |
|||
bash("docker volume create $volume"); |
|||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1"); |
|||
} |
|||
|
|||
println("http://localhost:$port"); |
@ -0,0 +1,59 @@ |
|||
console.log("== Uptime Kuma Reset Password Tool =="); |
|||
|
|||
console.log("Loading the database"); |
|||
|
|||
const Database = require("../server/database"); |
|||
const { R } = require("redbean-node"); |
|||
const readline = require("readline"); |
|||
const { initJWTSecret } = require("../server/util-server"); |
|||
const rl = readline.createInterface({ |
|||
input: process.stdin, |
|||
output: process.stdout |
|||
}); |
|||
|
|||
(async () => { |
|||
await Database.connect(); |
|||
|
|||
try { |
|||
const user = await R.findOne("user"); |
|||
|
|||
if (! user) { |
|||
throw new Error("user not found, have you installed?"); |
|||
} |
|||
|
|||
console.log("Found user: " + user.username); |
|||
|
|||
while (true) { |
|||
let password = await question("New Password: "); |
|||
let confirmPassword = await question("Confirm New Password: "); |
|||
|
|||
if (password === confirmPassword) { |
|||
await user.resetPassword(password); |
|||
|
|||
// Reset all sessions by reset jwt secret
|
|||
await initJWTSecret(); |
|||
|
|||
rl.close(); |
|||
break; |
|||
} else { |
|||
console.log("Passwords do not match, please try again."); |
|||
} |
|||
} |
|||
|
|||
console.log("Password reset successfully."); |
|||
} catch (e) { |
|||
console.error("Error: " + e.message); |
|||
} |
|||
|
|||
await Database.close(); |
|||
|
|||
console.log("Finished. You should restart the Uptime Kuma server.") |
|||
})(); |
|||
|
|||
function question(question) { |
|||
return new Promise((resolve) => { |
|||
rl.question(question, (answer) => { |
|||
resolve(answer); |
|||
}) |
|||
}); |
|||
} |
@ -0,0 +1,144 @@ |
|||
/* |
|||
* Simple DNS Server |
|||
* For testing DNS monitoring type, dev only |
|||
*/ |
|||
const dns2 = require("dns2"); |
|||
|
|||
const { Packet } = dns2; |
|||
|
|||
const server = dns2.createServer({ |
|||
udp: true |
|||
}); |
|||
|
|||
server.on("request", (request, send, rinfo) => { |
|||
for (let question of request.questions) { |
|||
console.log(question.name, type(question.type), question.class); |
|||
|
|||
const response = Packet.createResponseFromRequest(request); |
|||
|
|||
if (question.name === "existing.com") { |
|||
|
|||
if (question.type === Packet.TYPE.A) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
address: "1.2.3.4" |
|||
}); |
|||
} if (question.type === Packet.TYPE.AAAA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
address: "fe80::::1234:5678:abcd:ef00", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.CNAME) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
domain: "cname1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.MX) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
exchange: "mx1.existing.com", |
|||
priority: 5 |
|||
}); |
|||
} else if (question.type === Packet.TYPE.NS) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
ns: "ns1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.SOA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
primary: "existing.com", |
|||
admin: "admin@existing.com", |
|||
serial: 2021082701, |
|||
refresh: 300, |
|||
retry: 3, |
|||
expiration: 10, |
|||
minimum: 10, |
|||
}); |
|||
} else if (question.type === Packet.TYPE.SRV) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
priority: 5, |
|||
weight: 5, |
|||
port: 8080, |
|||
target: "srv1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.TXT) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
data: "#v=spf1 include:_spf.existing.com ~all", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.CAA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
flags: 0, |
|||
tag: "issue", |
|||
value: "ca.existing.com", |
|||
}); |
|||
} |
|||
|
|||
} |
|||
|
|||
if (question.name === "4.3.2.1.in-addr.arpa") { |
|||
if (question.type === Packet.TYPE.PTR) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
domain: "ptr1.existing.com", |
|||
}); |
|||
} |
|||
} |
|||
|
|||
send(response); |
|||
} |
|||
}); |
|||
|
|||
server.on("listening", () => { |
|||
console.log("Listening"); |
|||
console.log(server.addresses()); |
|||
}); |
|||
|
|||
server.on("close", () => { |
|||
console.log("server closed"); |
|||
}); |
|||
|
|||
server.listen({ |
|||
udp: 5300 |
|||
}); |
|||
|
|||
function type(code) { |
|||
for (let name in Packet.TYPE) { |
|||
if (Packet.TYPE[name] === code) { |
|||
return name; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
package-lock.json |
|||
test.js |
|||
languages/ |
@ -0,0 +1,78 @@ |
|||
// Need to use es6 to read language files
|
|||
|
|||
import fs from "fs"; |
|||
import path from "path"; |
|||
import util from "util"; |
|||
|
|||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
|||
/** |
|||
* Look ma, it's cp -R. |
|||
* @param {string} src The path to the thing to copy. |
|||
* @param {string} dest The path to the new copy. |
|||
*/ |
|||
const copyRecursiveSync = function (src, dest) { |
|||
let exists = fs.existsSync(src); |
|||
let stats = exists && fs.statSync(src); |
|||
let isDirectory = exists && stats.isDirectory(); |
|||
if (isDirectory) { |
|||
fs.mkdirSync(dest); |
|||
fs.readdirSync(src).forEach(function (childItemName) { |
|||
copyRecursiveSync(path.join(src, childItemName), |
|||
path.join(dest, childItemName)); |
|||
}); |
|||
} else { |
|||
fs.copyFileSync(src, dest); |
|||
} |
|||
}; |
|||
console.log(process.argv) |
|||
const baseLangCode = process.argv[2] || "zh-HK"; |
|||
console.log("Base Lang: " + baseLangCode); |
|||
fs.rmdirSync("./languages", { recursive: true }); |
|||
copyRecursiveSync("../../src/languages", "./languages"); |
|||
|
|||
const en = (await import("./languages/en.js")).default; |
|||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default; |
|||
const files = fs.readdirSync("./languages"); |
|||
console.log(files); |
|||
for (const file of files) { |
|||
if (file.endsWith(".js")) { |
|||
console.log("Processing " + file); |
|||
const lang = await import("./languages/" + file); |
|||
|
|||
let obj; |
|||
|
|||
if (lang.default) { |
|||
console.log("is js module"); |
|||
obj = lang.default; |
|||
} else { |
|||
console.log("empty file"); |
|||
obj = { |
|||
languageName: "<Your Language name in your language (not in English)>" |
|||
}; |
|||
} |
|||
|
|||
// En first
|
|||
for (const key in en) { |
|||
if (! obj[key]) { |
|||
obj[key] = en[key]; |
|||
} |
|||
} |
|||
|
|||
// Base second
|
|||
for (const key in baseLang) { |
|||
if (! obj[key]) { |
|||
obj[key] = key; |
|||
} |
|||
} |
|||
|
|||
const code = "export default " + util.inspect(obj, { |
|||
depth: null, |
|||
}); |
|||
|
|||
fs.writeFileSync(`../../src/languages/${file}`, code); |
|||
|
|||
} |
|||
} |
|||
|
|||
fs.rmdirSync("./languages", { recursive: true }); |
|||
console.log("Done, fix the format by eslint now"); |
@ -0,0 +1,12 @@ |
|||
{ |
|||
"name": "update-language-files", |
|||
"type": "module", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,61 @@ |
|||
const pkg = require("../package.json"); |
|||
const fs = require("fs"); |
|||
const child_process = require("child_process"); |
|||
const util = require("../src/util"); |
|||
|
|||
util.polyfill(); |
|||
|
|||
const oldVersion = pkg.version; |
|||
const newVersion = process.argv[2]; |
|||
|
|||
console.log("Old Version: " + oldVersion); |
|||
console.log("New Version: " + newVersion); |
|||
|
|||
if (! newVersion) { |
|||
console.error("invalid version"); |
|||
process.exit(1); |
|||
} |
|||
|
|||
const exists = tagExists(newVersion); |
|||
|
|||
if (! exists) { |
|||
// Process package.json
|
|||
pkg.version = newVersion; |
|||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); |
|||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); |
|||
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion); |
|||
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion); |
|||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); |
|||
|
|||
commit(newVersion); |
|||
tag(newVersion); |
|||
} else { |
|||
console.log("version exists") |
|||
} |
|||
|
|||
function commit(version) { |
|||
let msg = "update to " + version; |
|||
|
|||
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]); |
|||
let stdout = res.stdout.toString().trim(); |
|||
console.log(stdout) |
|||
|
|||
if (stdout.includes("no changes added to commit")) { |
|||
throw new Error("commit error") |
|||
} |
|||
} |
|||
|
|||
function tag(version) { |
|||
let res = child_process.spawnSync("git", ["tag", version]); |
|||
console.log(res.stdout.toString().trim()) |
|||
} |
|||
|
|||
function tagExists(version) { |
|||
if (! version) { |
|||
throw new Error("invalid version"); |
|||
} |
|||
|
|||
let res = child_process.spawnSync("git", ["tag", "-l", version]); |
|||
|
|||
return res.stdout.toString().trim() === version; |
|||
} |
@ -1,39 +0,0 @@ |
|||
/** |
|||
* String.prototype.replaceAll() polyfill |
|||
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
|||
* @author Chris Ferdinandi |
|||
* @license MIT |
|||
*/ |
|||
if (!String.prototype.replaceAll) { |
|||
String.prototype.replaceAll = function(str, newStr){ |
|||
|
|||
// If a regex pattern
|
|||
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') { |
|||
return this.replace(str, newStr); |
|||
} |
|||
|
|||
// If a string
|
|||
return this.replace(new RegExp(str, 'g'), newStr); |
|||
|
|||
}; |
|||
} |
|||
|
|||
const pkg = require('../package.json'); |
|||
const fs = require("fs"); |
|||
const oldVersion = pkg.version |
|||
const newVersion = process.argv[2] |
|||
|
|||
console.log("Old Version: " + oldVersion) |
|||
console.log("New Version: " + newVersion) |
|||
|
|||
if (newVersion) { |
|||
// Process package.json
|
|||
pkg.version = newVersion |
|||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion) |
|||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion) |
|||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n") |
|||
|
|||
// Process README.md
|
|||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) |
|||
} |
|||
|
@ -1,16 +1,16 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> |
|||
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<meta name="theme-color" content="#5cdd8b" /> |
|||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> |
|||
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> |
|||
<meta name="theme-color" id="theme-color" content="" /> |
|||
<meta name="description" content="Uptime Kuma monitoring tool" /> |
|||
<title>Uptime Kuma</title> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.js"></script> |
|||
</body> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.js"></script> |
|||
</body> |
|||
</html> |
|||
|
@ -0,0 +1,203 @@ |
|||
# install.sh is generated by ./extra/install.batsh, do not modify it directly. |
|||
# "npm run compile-install-script" to compile install.sh |
|||
# The command is working on Windows PowerShell and Docker for Windows only. |
|||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh |
|||
"echo" "-e" "=====================" |
|||
"echo" "-e" "Uptime Kuma Installer" |
|||
"echo" "-e" "=====================" |
|||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian" |
|||
"echo" "-e" "---------------------------------------" |
|||
"echo" "-e" "This script is designed for Linux and basic usage." |
|||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation" |
|||
"echo" "-e" "---------------------------------------" |
|||
"echo" "-e" "" |
|||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2" |
|||
"echo" "-e" "Docker - Install Uptime Kuma Docker container" |
|||
"echo" "-e" "" |
|||
if [ "$1" != "" ]; then |
|||
type="$1" |
|||
else |
|||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type" |
|||
fi |
|||
defaultPort="3001" |
|||
function checkNode { |
|||
local _0 |
|||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])') |
|||
"echo" "-e" "Node Version: ""$nodeVersion" |
|||
_0="12" |
|||
if [ $(($nodeVersion < $_0)) == 1 ]; then |
|||
"echo" "-e" "Error: Required Node.js 14" |
|||
"exit" "1" |
|||
fi |
|||
if [ "$nodeVersion" == "12" ]; then |
|||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested." |
|||
fi |
|||
} |
|||
function deb { |
|||
nodeCheck=$(node -v) |
|||
apt --yes update |
|||
if [ "$nodeCheck" != "" ]; then |
|||
"checkNode" |
|||
else |
|||
# Old nodejs binary name is "nodejs" |
|||
check=$(nodejs --version) |
|||
if [ "$check" != "" ]; then |
|||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old." |
|||
exit 1 |
|||
fi |
|||
curlCheck=$(curl --version) |
|||
if [ "$curlCheck" == "" ]; then |
|||
"echo" "-e" "Installing Curl" |
|||
apt --yes install curl |
|||
fi |
|||
"echo" "-e" "Installing Node.js 14" |
|||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt |
|||
apt --yes install nodejs |
|||
node -v |
|||
nodeCheckAgain=$(node -v) |
|||
if [ "$nodeCheckAgain" == "" ]; then |
|||
"echo" "-e" "Error during Node.js installation" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing Git" |
|||
apt --yes install git |
|||
fi |
|||
} |
|||
if [ "$type" == "local" ]; then |
|||
defaultInstallPath="/opt/uptime-kuma" |
|||
if [ -e "/etc/redhat-release" ]; then |
|||
os=$("cat" "/etc/redhat-release") |
|||
distribution="rhel" |
|||
else |
|||
if [ -e "/etc/issue" ]; then |
|||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ') |
|||
if [ "$os" == "Ubuntu" ]; then |
|||
distribution="ubuntu" |
|||
fi |
|||
if [ "$os" == "Debian" ]; then |
|||
distribution="debian" |
|||
fi |
|||
fi |
|||
fi |
|||
arch=$(uname -i) |
|||
"echo" "-e" "Your OS: ""$os" |
|||
"echo" "-e" "Distribution: ""$distribution" |
|||
"echo" "-e" "Arch: ""$arch" |
|||
if [ "$3" != "" ]; then |
|||
port="$3" |
|||
else |
|||
"read" "-p" "Listening Port [$defaultPort]: " "port" |
|||
if [ "$port" == "" ]; then |
|||
port="$defaultPort" |
|||
fi |
|||
fi |
|||
if [ "$2" != "" ]; then |
|||
installPath="$2" |
|||
else |
|||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath" |
|||
if [ "$installPath" == "" ]; then |
|||
installPath="$defaultInstallPath" |
|||
fi |
|||
fi |
|||
# CentOS |
|||
if [ "$distribution" == "rhel" ]; then |
|||
nodeCheck=$(node -v) |
|||
if [ "$nodeCheck" != "" ]; then |
|||
"checkNode" |
|||
else |
|||
curlCheck=$(curl --version) |
|||
if [ "$curlCheck" == "" ]; then |
|||
"echo" "-e" "Installing Curl" |
|||
yum -y -q install curl |
|||
fi |
|||
"echo" "-e" "Installing Node.js 14" |
|||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt |
|||
yum install -y -q nodejs |
|||
node -v |
|||
nodeCheckAgain=$(node -v) |
|||
if [ "$nodeCheckAgain" == "" ]; then |
|||
"echo" "-e" "Error during Node.js installation" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing Git" |
|||
yum -y -q install git |
|||
fi |
|||
# Ubuntu |
|||
else |
|||
if [ "$distribution" == "ubuntu" ]; then |
|||
"deb" |
|||
# Debian |
|||
else |
|||
if [ "$distribution" == "debian" ]; then |
|||
"deb" |
|||
else |
|||
# Unknown distribution |
|||
error=$((0)) |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
error=$((1)) |
|||
"echo" "-e" "Error: git is missing" |
|||
fi |
|||
check=$(node -v) |
|||
if [ "$check" == "" ]; then |
|||
error=$((1)) |
|||
"echo" "-e" "Error: node is missing" |
|||
fi |
|||
if [ $(($error > 0)) == 1 ]; then |
|||
"echo" "-e" "Please install above missing software" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
fi |
|||
fi |
|||
check=$(pm2 --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing PM2" |
|||
npm install pm2 -g |
|||
pm2 startup |
|||
fi |
|||
mkdir -p $installPath |
|||
cd $installPath |
|||
git clone https://github.com/louislam/uptime-kuma.git . |
|||
npm run setup |
|||
pm2 start server/server.js --name uptime-kuma -- --port=$port |
|||
else |
|||
defaultVolume="uptime-kuma" |
|||
check=$(docker -v) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Error: docker is not found!" |
|||
exit 1 |
|||
fi |
|||
check=$(docker info) |
|||
if [[ "$check" == *"Is the docker daemon running"* ]]; then |
|||
"echo" "Error: docker is not running" |
|||
"exit" "1" |
|||
fi |
|||
if [ "$3" != "" ]; then |
|||
port="$3" |
|||
else |
|||
"read" "-p" "Expose Port [$defaultPort]: " "port" |
|||
if [ "$port" == "" ]; then |
|||
port="$defaultPort" |
|||
fi |
|||
fi |
|||
if [ "$2" != "" ]; then |
|||
volume="$2" |
|||
else |
|||
"read" "-p" "Volume Name [$defaultVolume]: " "volume" |
|||
if [ "$volume" == "" ]; then |
|||
volume="$defaultVolume" |
|||
fi |
|||
fi |
|||
"echo" "-e" "Port: $port" |
|||
"echo" "-e" "Volume: $volume" |
|||
docker volume create $volume |
|||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1 |
|||
fi |
|||
"echo" "-e" "http://localhost:$port" |
@ -0,0 +1,31 @@ |
|||
# Uptime-Kuma K8s Deployment |
|||
|
|||
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk. |
|||
|
|||
## How does it work? |
|||
|
|||
Kustomize is a tool which builds a complete deployment file for all config elements. |
|||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing. |
|||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like. |
|||
|
|||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service |
|||
|
|||
## What do i have to edit? |
|||
You have to edit the ```ingressroute.yml``` to your needs. |
|||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/). |
|||
|
|||
- host |
|||
- secrets and secret names |
|||
- (Cluster)Issuer (optional) |
|||
- the Version in the Deployment-File |
|||
- update: |
|||
- change to newer version and run the above commands, it will update the pods one after another |
|||
|
|||
## How To use: |
|||
|
|||
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) |
|||
- Edit files mentioned above to your needs |
|||
- run ```kustomize build > apply.yml``` |
|||
- run ```kubectl apply -f apply.yml``` |
|||
|
|||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address. |
@ -0,0 +1,10 @@ |
|||
namespace: uptime-kuma |
|||
namePrefix: uptime-kuma- |
|||
|
|||
commonLabels: |
|||
app: uptime-kuma |
|||
|
|||
bases: |
|||
- uptime-kuma |
|||
|
|||
|
@ -0,0 +1,45 @@ |
|||
apiVersion: apps/v1 |
|||
kind: Deployment |
|||
metadata: |
|||
labels: |
|||
component: uptime-kuma |
|||
name: deployment |
|||
spec: |
|||
selector: |
|||
matchLabels: |
|||
component: uptime-kuma |
|||
replicas: 1 |
|||
strategy: |
|||
type: Recreate |
|||
|
|||
template: |
|||
metadata: |
|||
labels: |
|||
component: uptime-kuma |
|||
spec: |
|||
containers: |
|||
- name: app |
|||
image: louislam/uptime-kuma:1 |
|||
ports: |
|||
- containerPort: 3001 |
|||
volumeMounts: |
|||
- mountPath: /app/data |
|||
name: storage |
|||
livenessProbe: |
|||
exec: |
|||
command: |
|||
- node |
|||
- extra/healthcheck.js |
|||
initialDelaySeconds: 180 |
|||
periodSeconds: 60 |
|||
timeoutSeconds: 30 |
|||
readinessProbe: |
|||
httpGet: |
|||
path: / |
|||
port: 3001 |
|||
scheme: HTTP |
|||
|
|||
volumes: |
|||
- name: storage |
|||
persistentVolumeClaim: |
|||
claimName: pvc |
@ -0,0 +1,39 @@ |
|||
apiVersion: networking.k8s.io/v1 |
|||
kind: Ingress |
|||
metadata: |
|||
annotations: |
|||
kubernetes.io/ingress.class: nginx |
|||
cert-manager.io/cluster-issuer: letsencrypt-prod |
|||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" |
|||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" |
|||
nginx.ingress.kubernetes.io/server-snippets: | |
|||
location / { |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_http_version 1.1; |
|||
proxy_set_header X-Forwarded-Host $http_host; |
|||
proxy_set_header X-Forwarded-Proto $scheme; |
|||
proxy_set_header X-Forwarded-For $remote_addr; |
|||
proxy_set_header Host $host; |
|||
proxy_set_header Connection "upgrade"; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_cache_bypass $http_upgrade; |
|||
} |
|||
name: ingress |
|||
spec: |
|||
tls: |
|||
- hosts: |
|||
- example.com |
|||
secretName: example-com-tls |
|||
rules: |
|||
- host: example.com |
|||
http: |
|||
paths: |
|||
- path: / |
|||
pathType: Prefix |
|||
backend: |
|||
service: |
|||
name: service |
|||
port: |
|||
number: 3001 |
@ -0,0 +1,5 @@ |
|||
resources: |
|||
- deployment.yml |
|||
- service.yml |
|||
- ingressroute.yml |
|||
- pvc.yml |
@ -0,0 +1,10 @@ |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: pvc |
|||
spec: |
|||
accessModes: |
|||
- ReadWriteOnce |
|||
resources: |
|||
requests: |
|||
storage: 4Gi |
@ -0,0 +1,13 @@ |
|||
apiVersion: v1 |
|||
kind: Service |
|||
metadata: |
|||
name: service |
|||
spec: |
|||
selector: |
|||
component: uptime-kuma |
|||
type: ClusterIP |
|||
ports: |
|||
- name: http |
|||
port: 3001 |
|||
targetPort: 3001 |
|||
protocol: TCP |
File diff suppressed because it is too large
After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 15 KiB |
@ -1,3 +0,0 @@ |
|||
# https://www.robotstxt.org/robotstxt.html |
|||
User-agent: * |
|||
Disallow: |
@ -0,0 +1,44 @@ |
|||
const { setSetting } = require("./util-server"); |
|||
const axios = require("axios"); |
|||
const { isDev } = require("../src/util"); |
|||
|
|||
exports.version = require("../package.json").version; |
|||
exports.latestVersion = null; |
|||
|
|||
let interval; |
|||
|
|||
exports.startInterval = () => { |
|||
let check = async () => { |
|||
try { |
|||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json"); |
|||
|
|||
if (typeof res.data === "string") { |
|||
res.data = JSON.parse(res.data); |
|||
} |
|||
|
|||
// For debug
|
|||
if (process.env.TEST_CHECK_VERSION === "1") { |
|||
res.data.version = "1000.0.0" |
|||
} |
|||
|
|||
exports.latestVersion = res.data.version; |
|||
console.log("Latest Version: " + exports.latestVersion); |
|||
} catch (_) { } |
|||
|
|||
}; |
|||
|
|||
check(); |
|||
interval = setInterval(check, 3600 * 1000 * 48); |
|||
}; |
|||
|
|||
exports.enableCheckUpdate = async (value) => { |
|||
await setSetting("checkUpdate", value); |
|||
|
|||
clearInterval(interval); |
|||
|
|||
if (value) { |
|||
exports.startInterval(); |
|||
} |
|||
}; |
|||
|
|||
exports.socket = null; |
@ -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, |
|||
} |
@ -0,0 +1,21 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
const passwordHash = require("../password-hash"); |
|||
const { R } = require("redbean-node"); |
|||
|
|||
class User extends BeanModel { |
|||
|
|||
/** |
|||
* Direct execute, no need R.store() |
|||
* @param newPassword |
|||
* @returns {Promise<void>} |
|||
*/ |
|||
async resetPassword(newPassword) { |
|||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ |
|||
passwordHash.generate(newPassword), |
|||
this.id |
|||
]); |
|||
this.password = newPassword; |
|||
} |
|||
} |
|||
|
|||
module.exports = User; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -1,8 +1,20 @@ |
|||
$primary: #5CDD8B; |
|||
$danger: #DC3545; |
|||
$primary: #5cdd8b; |
|||
$danger: #dc3545; |
|||
$warning: #f8a306; |
|||
$link-color: #111; |
|||
$border-radius: 50rem; |
|||
|
|||
$highlight: #7ce8a4; |
|||
$highlight-white: #e7faec; |
|||
|
|||
$dark-font-color: #b1b8c0; |
|||
$dark-font-color2: #020b05; |
|||
$dark-bg: #0d1117; |
|||
$dark-bg2: #070a10; |
|||
$dark-border-color: #1d2634; |
|||
|
|||
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97); |
|||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); |
|||
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86); |
|||
|
|||
$dropdown-border-radius: 0.5rem; |
|||
|
@ -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> |
@ -0,0 +1,143 @@ |
|||
<template> |
|||
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }"> |
|||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3"> |
|||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> |
|||
</div> |
|||
|
|||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }"> |
|||
<div class="row"> |
|||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"> |
|||
<div class="info"> |
|||
<Uptime :monitor="item" type="24" :pill="true" /> |
|||
{{ item.name }} |
|||
</div> |
|||
</div> |
|||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4"> |
|||
<HeartbeatBar size="small" :monitor-id="item.id" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row"> |
|||
<div class="col-12"> |
|||
<HeartbeatBar size="small" :monitor-id="item.id" /> |
|||
</div> |
|||
</div> |
|||
</router-link> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import HeartbeatBar from "../components/HeartbeatBar.vue"; |
|||
import Uptime from "../components/Uptime.vue"; |
|||
export default { |
|||
components: { |
|||
Uptime, |
|||
HeartbeatBar, |
|||
}, |
|||
props: { |
|||
scrollbar: { |
|||
type: Boolean, |
|||
}, |
|||
}, |
|||
computed: { |
|||
sortedMonitorList() { |
|||
let result = Object.values(this.$root.monitorList); |
|||
|
|||
result.sort((m1, m2) => { |
|||
|
|||
if (m1.active !== m2.active) { |
|||
if (m1.active === 0) { |
|||
return 1; |
|||
} |
|||
|
|||
if (m2.active === 0) { |
|||
return -1; |
|||
} |
|||
} |
|||
|
|||
if (m1.weight !== m2.weight) { |
|||
if (m1.weight > m2.weight) { |
|||
return -1; |
|||
} |
|||
|
|||
if (m1.weight < m2.weight) { |
|||
return 1; |
|||
} |
|||
} |
|||
|
|||
return m1.name.localeCompare(m2.name); |
|||
}) |
|||
|
|||
return result; |
|||
}, |
|||
}, |
|||
methods: { |
|||
monitorURL(id) { |
|||
return "/dashboard/" + id; |
|||
}, |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
@import "../assets/vars.scss"; |
|||
|
|||
.small-padding { |
|||
padding-left: 5px !important; |
|||
padding-right: 5px !important; |
|||
} |
|||
|
|||
.list { |
|||
&.scrollbar { |
|||
min-height: calc(100vh - 240px); |
|||
max-height: calc(100vh - 30px); |
|||
overflow-y: auto; |
|||
position: sticky; |
|||
top: 10px; |
|||
} |
|||
|
|||
.item { |
|||
display: block; |
|||
text-decoration: none; |
|||
padding: 13px 15px 10px 15px; |
|||
border-radius: 10px; |
|||
transition: all ease-in-out 0.15s; |
|||
|
|||
&.disabled { |
|||
opacity: 0.3; |
|||
} |
|||
|
|||
.info { |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
&:hover { |
|||
background-color: $highlight-white; |
|||
} |
|||
|
|||
&.active { |
|||
background-color: #cdf8f4; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.dark { |
|||
.list { |
|||
.item { |
|||
&:hover { |
|||
background-color: $dark-bg2; |
|||
} |
|||
|
|||
&.active { |
|||
background-color: $dark-bg2; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.monitorItem { |
|||
width: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,176 @@ |
|||
<template> |
|||
<LineChart :chart-data="chartData" :options="chartOptions" /> |
|||
</template> |
|||
|
|||
<script> |
|||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js"; |
|||
import dayjs from "dayjs"; |
|||
import utc from "dayjs/plugin/utc"; |
|||
import timezone from "dayjs/plugin/timezone"; |
|||
import "chartjs-adapter-dayjs"; |
|||
import { LineChart } from "vue-chart-3"; |
|||
dayjs.extend(utc); |
|||
dayjs.extend(timezone); |
|||
|
|||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler); |
|||
|
|||
export default { |
|||
components: { LineChart }, |
|||
props: { |
|||
monitorId: { |
|||
type: Number, |
|||
required: true, |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
// Configurable filtering on top of the returned data |
|||
chartPeriodHrs: 6, |
|||
}; |
|||
}, |
|||
computed: { |
|||
chartOptions() { |
|||
return { |
|||
responsive: true, |
|||
maintainAspectRatio: false, |
|||
onResize: (chart) => { |
|||
chart.canvas.parentNode.style.position = "relative"; |
|||
if (screen.width < 576) { |
|||
chart.canvas.parentNode.style.height = "275px"; |
|||
} else if (screen.width < 768) { |
|||
chart.canvas.parentNode.style.height = "320px"; |
|||
} else if (screen.width < 992) { |
|||
chart.canvas.parentNode.style.height = "300px"; |
|||
} else { |
|||
chart.canvas.parentNode.style.height = "250px"; |
|||
} |
|||
}, |
|||
layout: { |
|||
padding: { |
|||
left: 10, |
|||
right: 30, |
|||
top: 30, |
|||
bottom: 10, |
|||
}, |
|||
}, |
|||
|
|||
elements: { |
|||
point: { |
|||
// Hide points on chart unless mouse-over |
|||
radius: 0, |
|||
hitRadius: 100, |
|||
}, |
|||
}, |
|||
scales: { |
|||
x: { |
|||
type: "time", |
|||
time: { |
|||
minUnit: "minute", |
|||
round: "second", |
|||
tooltipFormat: "YYYY-MM-DD HH:mm:ss", |
|||
displayFormats: { |
|||
minute: "HH:mm", |
|||
hour: "MM-DD HH:mm", |
|||
} |
|||
}, |
|||
ticks: { |
|||
maxRotation: 0, |
|||
autoSkipPadding: 30, |
|||
}, |
|||
grid: { |
|||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)", |
|||
offset: false, |
|||
}, |
|||
}, |
|||
y: { |
|||
title: { |
|||
display: true, |
|||
text: this.$t("respTime"), |
|||
}, |
|||
offset: false, |
|||
grid: { |
|||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)", |
|||
}, |
|||
}, |
|||
y1: { |
|||
display: false, |
|||
position: "right", |
|||
grid: { |
|||
drawOnChartArea: false, |
|||
}, |
|||
min: 0, |
|||
max: 1, |
|||
offset: false, |
|||
}, |
|||
}, |
|||
bounds: "ticks", |
|||
plugins: { |
|||
tooltip: { |
|||
mode: "nearest", |
|||
intersect: false, |
|||
padding: 10, |
|||
backgroundColor: this.$root.theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)", |
|||
bodyColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)", |
|||
titleColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)", |
|||
filter: function (tooltipItem) { |
|||
return tooltipItem.datasetIndex === 0; // Hide tooltip on Bar Chart |
|||
}, |
|||
callbacks: { |
|||
label: (context) => { |
|||
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms` |
|||
}, |
|||
} |
|||
}, |
|||
legend: { |
|||
display: false, |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
chartData() { |
|||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time |
|||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up |
|||
if (this.monitorId in this.$root.heartbeatList) { |
|||
this.$root.heartbeatList[this.monitorId] |
|||
.filter( |
|||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours"))) |
|||
.map((beat) => { |
|||
const x = this.$root.datetime(beat.time); |
|||
pingData.push({ |
|||
x, |
|||
y: beat.ping, |
|||
}); |
|||
downData.push({ |
|||
x, |
|||
y: beat.status === 0 ? 1 : 0, |
|||
}) |
|||
}); |
|||
} |
|||
return { |
|||
datasets: [ |
|||
{ |
|||
// Line Chart |
|||
data: pingData, |
|||
fill: "origin", |
|||
tension: 0.2, |
|||
borderColor: "#5CDD8B", |
|||
backgroundColor: "#5CDD8B38", |
|||
yAxisID: "y", |
|||
}, |
|||
{ |
|||
// Bar Chart |
|||
type: "bar", |
|||
data: downData, |
|||
borderColor: "#00000000", |
|||
backgroundColor: "#DC354568", |
|||
yAxisID: "y1", |
|||
barThickness: "flex", |
|||
barPercentage: 1, |
|||
categoryPercentage: 1, |
|||
}, |
|||
], |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
@ -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=""Uptime Kuma" <example@kuma.pet>"> |
|||
<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> |
@ -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> |
@ -1,10 +1,10 @@ |
|||
import { library } from "@fortawesome/fontawesome-svg-core" |
|||
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons" |
|||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, 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, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList) |
|||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash); |
|||
|
|||
export { FontAwesomeIcon } |
|||
|
@ -0,0 +1,18 @@ |
|||
# How to translate |
|||
|
|||
1. Fork this repo. |
|||
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm |
|||
3. `npm run update-language-files --base-lang=de-DE` |
|||
6. Your language file should be filled in. You can translate now. |
|||
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). |
|||
8. Import your language file in `src/main.js` and add it to `languageList` constant. |
|||
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done. |
|||
|
|||
|
|||
|
|||
One of good examples: |
|||
https://github.com/louislam/uptime-kuma/pull/316/files |
|||
|
|||
|
|||
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏 |
|||
|
@ -0,0 +1,131 @@ |
|||
export default { |
|||
languageName: "Danish", |
|||
Settings: "Indstillinger", |
|||
Dashboard: "Dashboard", |
|||
"New Update": "Opdatering tilgængelig", |
|||
Language: "Sprog", |
|||
Appearance: "Udseende", |
|||
Theme: "Tema", |
|||
General: "Generelt", |
|||
Version: "Version", |
|||
"Check Update On GitHub": "Tjek efter opdateringer på Github", |
|||
List: "Liste", |
|||
Add: "Tilføj", |
|||
"Add New Monitor": "Tilføj ny Overvåger", |
|||
"Quick Stats": "Oversigt", |
|||
Up: "Aktiv", |
|||
Down: "Inaktiv", |
|||
Pending: "Afventer", |
|||
Unknown: "Ukendt", |
|||
Pause: "Pause", |
|||
pauseDashboardHome: "Pauset", |
|||
Name: "Navn", |
|||
Status: "Status", |
|||
DateTime: "Dato / Tid", |
|||
Message: "Beskeder", |
|||
"No important events": "Inden vigtige begivenheder", |
|||
Resume: "Fortsæt", |
|||
Edit: "Rediger", |
|||
Delete: "Slet", |
|||
Current: "Aktuelt", |
|||
Uptime: "Oppetid", |
|||
"Cert Exp.": "Certifikatets udløb", |
|||
days: "Dage", |
|||
day: "Dag", |
|||
"-day": "-Dage", |
|||
hour: "Timer", |
|||
"-hour": "-Timer", |
|||
checkEverySecond: "Tjek hvert {0} sekund", |
|||
"Avg.": "Gennemsnit", |
|||
Response: " Respons", |
|||
Ping: "Ping", |
|||
"Monitor Type": "Overvåger Type", |
|||
Keyword: "Nøgleord", |
|||
"Friendly Name": "Visningsnavn", |
|||
URL: "URL", |
|||
Hostname: "Hostname", |
|||
Port: "Port", |
|||
"Heartbeat Interval": "Taktinterval", |
|||
Retries: "Gentagelser", |
|||
retriesDescription: "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.", |
|||
Advanced: "Avanceret", |
|||
ignoreTLSError: "Ignorere TLS/SSL web fejl", |
|||
"Upside Down Mode": "Omvendt tilstand", |
|||
upsideDownModeDescription: "Håndter tilstanden omvendt. Hvis tjenesten er tilgængelig, vises den som inaktiv.", |
|||
"Max. Redirects": "Maks. Omdirigeringer", |
|||
maxRedirectDescription: "Maksimalt antal omdirigeringer, der skal følges. Indstil til 0 for at deaktivere omdirigeringer.", |
|||
"Accepted Status Codes": "Tilladte HTTP-Statuskoder", |
|||
acceptedStatusCodesDescription: "Vælg de statuskoder, der stadig skal vurderes som vellykkede.", |
|||
Save: "Gem", |
|||
Notifications: "Underretninger", |
|||
"Not available, please setup.": "Ikke tilgængelige, opsæt venligst.", |
|||
"Setup Notification": "Opsæt underretninger", |
|||
Light: "Lys", |
|||
Dark: "Mørk", |
|||
Auto: "Auto", |
|||
"Theme - Heartbeat Bar": "Tema - Tidslinje", |
|||
Normal: "Normal", |
|||
Bottom: "Bunden", |
|||
None: "Ingen", |
|||
Timezone: "Tidszone", |
|||
"Search Engine Visibility": "Søgemaskine synlighed", |
|||
"Allow indexing": "Tillad indeksering", |
|||
"Discourage search engines from indexing site": "Frabed søgemaskiner at indeksere webstedet", |
|||
"Change Password": "Ændre adgangskode", |
|||
"Current Password": "Nuværende adgangskode", |
|||
"New Password": "Ny adgangskode", |
|||
"Repeat New Password": "Gentag den nye adgangskode", |
|||
passwordNotMatchMsg: "Adgangskoderne er ikke ens.", |
|||
"Update Password": "Opdater adgangskode", |
|||
"Disable Auth": "Deaktiver autentificering", |
|||
"Enable Auth": "Aktiver autentificering", |
|||
Logout: "Log ud", |
|||
notificationDescription: "Tildel underretninger til Overvåger(e), så denne funktion træder i kraft.", |
|||
Leave: "Verlassen", |
|||
"I understand, please disable": "Jeg er indforstået, deaktiver venligst", |
|||
Confirm: "Bekræft", |
|||
Yes: "Ja", |
|||
No: "Nej", |
|||
Username: "Brugernavn", |
|||
Password: "Adgangskode", |
|||
"Remember me": "Husk mig", |
|||
Login: "Log ind", |
|||
"No Monitors, please": "Ingen Overvågere", |
|||
"add one": "tilføj en", |
|||
"Notification Type": "Underretningstype", |
|||
Email: "E-Mail", |
|||
Test: "Test", |
|||
"Certificate Info": "Certifikatoplysninger", |
|||
keywordDescription: "Søg efter et søgeord i almindelig HTML- eller JSON -output. Bemærk, at der skelnes mellem store og små bogstaver.", |
|||
deleteMonitorMsg: "Er du sikker på, at du vil slette overvågeren?", |
|||
deleteNotificationMsg: "Er du sikker på, at du vil slette denne underretning for alle overvågere? ", |
|||
resoverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.", |
|||
"Resolver Server": "Navne-server", |
|||
rrtypeDescription: "Vælg den type RR, du vil overvåge.", |
|||
"Last Result": "Seneste resultat", |
|||
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?", |
|||
"Create your admin account": "Opret din administratorkonto", |
|||
"Repeat Password": "Gentag adgangskoden", |
|||
"Resource Record Type": "Resource Record Type", |
|||
respTime: "Resp. Time (ms)", |
|||
notAvailableShort: "N/A", |
|||
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." |
|||
} |
@ -0,0 +1,132 @@ |
|||
export default { |
|||
languageName: "German", |
|||
Settings: "Einstellungen", |
|||
Dashboard: "Dashboard", |
|||
"New Update": "Update Verfügbar", |
|||
Language: "Sprache", |
|||
Appearance: "Erscheinung", |
|||
Theme: "Thema", |
|||
General: "Allgemein", |
|||
Version: "Version", |
|||
"Check Update On GitHub": "Überprüfen von Updates auf Github", |
|||
List: "Liste", |
|||
Add: "Hinzufügen", |
|||
"Add New Monitor": "Neuer Monitor", |
|||
"Quick Stats": "Übersicht", |
|||
Up: "Aktiv", |
|||
Down: "Inaktiv", |
|||
Pending: "Ausstehend", |
|||
Unknown: "Unbekannt", |
|||
Pause: "Pausieren", |
|||
pauseDashboardHome: "Pausiert", |
|||
Name: "Name", |
|||
Status: "Status", |
|||
DateTime: "Datum / Uhrzeit", |
|||
Message: "Nachricht", |
|||
"No important events": "Keine wichtigen Ereignisse", |
|||
Resume: "Fortsetzen", |
|||
Edit: "Bearbeiten", |
|||
Delete: "Löschen", |
|||
Current: "Aktuell", |
|||
Uptime: "Verfügbarkeit", |
|||
"Cert Exp.": "Zertifikatsablauf", |
|||
days: "Tage", |
|||
day: "Tag", |
|||
"-day": "-Tage", |
|||
hour: "Stunde", |
|||
"-hour": "-Stunden", |
|||
checkEverySecond: "Überprüfe alle {0} Sekunden", |
|||
"Avg.": "Durchschn. ", |
|||
Response: " Antwortzeit", |
|||
Ping: "Ping", |
|||
"Monitor Type": "Monitor Typ", |
|||
Keyword: "Schlüsselwort", |
|||
"Friendly Name": "Anzeigename", |
|||
URL: "URL", |
|||
Hostname: "Hostname", |
|||
Port: "Port", |
|||
"Heartbeat Interval": "Taktintervall", |
|||
Retries: "Wiederholungen", |
|||
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.", |
|||
Advanced: "Erweitert", |
|||
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten", |
|||
"Upside Down Mode": "Umgedrehter Modus", |
|||
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.", |
|||
"Max. Redirects": "Max. Weiterleitungen", |
|||
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.", |
|||
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes", |
|||
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.", |
|||
Save: "Speichern", |
|||
Notifications: "Benachrichtigungen", |
|||
"Not available, please setup.": "Keine verfügbar, bitte einrichten.", |
|||
"Setup Notification": "Benachrichtigung einrichten", |
|||
Light: "Hell", |
|||
Dark: "Dunkel", |
|||
Auto: "Auto", |
|||
"Theme - Heartbeat Bar": "Thema - Taktleiste", |
|||
Normal: "Normal", |
|||
Bottom: "Unten", |
|||
None: "Keine", |
|||
Timezone: "Zeitzone", |
|||
"Search Engine Visibility": "Suchmaschinensichtbarkeit", |
|||
"Allow indexing": "Indizierung zulassen", |
|||
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab", |
|||
"Change Password": "Passwort ändern", |
|||
"Current Password": "Dezeitiges Passwort", |
|||
"New Password": "Neues Passwort", |
|||
"Repeat New Password": "Wiederhole neues Passwort", |
|||
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ", |
|||
"Update Password": "Ändere Passwort", |
|||
"Disable Auth": "Authentifizierung deaktivieren", |
|||
"Enable Auth": "Authentifizierung aktivieren", |
|||
Logout: "Ausloggen", |
|||
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.", |
|||
Leave: "Verlassen", |
|||
"I understand, please disable": "Ich verstehe, bitte deaktivieren", |
|||
Confirm: "Bestätige", |
|||
Yes: "Ja", |
|||
No: "Nein", |
|||
Username: "Benutzername", |
|||
Password: "Passwort", |
|||
"Remember me": "Passwort merken", |
|||
Login: "Einloggen", |
|||
"No Monitors, please": "Keine Monitore, bitte", |
|||
"add one": "hinzufügen", |
|||
"Notification Type": "Benachrichtigungs Dienst", |
|||
Email: "E-Mail", |
|||
Test: "Test", |
|||
"Certificate Info": "Zertifikatsinfo", |
|||
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.", |
|||
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?", |
|||
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?", |
|||
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.", |
|||
"Resolver Server": "Auflösungsserver", |
|||
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.", |
|||
"Last Result": "Letztes Ergebnis", |
|||
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?", |
|||
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", |
|||
"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" |
|||
} |
@ -0,0 +1,132 @@ |
|||
export default { |
|||
languageName: "English", |
|||
checkEverySecond: "Check every {0} seconds.", |
|||
"Avg.": "Avg. ", |
|||
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent", |
|||
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites", |
|||
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", |
|||
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.", |
|||
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.", |
|||
passwordNotMatchMsg: "The repeat password does not match.", |
|||
notificationDescription: "Please assign a notification to monitor(s) to get it to work.", |
|||
keywordDescription: "Search keyword in plain html or JSON response and it is case-sensitive", |
|||
pauseDashboardHome: "Pause", |
|||
deleteMonitorMsg: "Are you sure want to delete this monitor?", |
|||
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", |
|||
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.", |
|||
rrtypeDescription: "Select the RR-Type you want to monitor", |
|||
pauseMonitorMsg: "Are you sure want to pause?", |
|||
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", |
|||
Language: "Language", |
|||
Appearance: "Appearance", |
|||
Theme: "Theme", |
|||
General: "General", |
|||
Version: "Version", |
|||
"Check Update On GitHub": "Check Update On GitHub", |
|||
List: "List", |
|||
Add: "Add", |
|||
"Add New Monitor": "Add New Monitor", |
|||
"Quick Stats": "Quick Stats", |
|||
Up: "Up", |
|||
Down: "Down", |
|||
Pending: "Pending", |
|||
Unknown: "Unknown", |
|||
Pause: "Pause", |
|||
Name: "Name", |
|||
Status: "Status", |
|||
DateTime: "DateTime", |
|||
Message: "Message", |
|||
"No important events": "No important events", |
|||
Resume: "Resume", |
|||
Edit: "Edit", |
|||
Delete: "Delete", |
|||
Current: "Current", |
|||
Uptime: "Uptime", |
|||
"Cert Exp.": "Cert Exp.", |
|||
days: "days", |
|||
day: "day", |
|||
"-day": "-day", |
|||
hour: "hour", |
|||
"-hour": "-hour", |
|||
Response: "Response", |
|||
Ping: "Ping", |
|||
"Monitor Type": "Monitor Type", |
|||
Keyword: "Keyword", |
|||
"Friendly Name": "Friendly Name", |
|||
URL: "URL", |
|||
Hostname: "Hostname", |
|||
Port: "Port", |
|||
"Heartbeat Interval": "Heartbeat Interval", |
|||
Retries: "Retries", |
|||
Advanced: "Advanced", |
|||
"Upside Down Mode": "Upside Down Mode", |
|||
"Max. Redirects": "Max. Redirects", |
|||
"Accepted Status Codes": "Accepted Status Codes", |
|||
Save: "Save", |
|||
Notifications: "Notifications", |
|||
"Not available, please setup.": "Not available, please setup.", |
|||
"Setup Notification": "Setup Notification", |
|||
Light: "Light", |
|||
Dark: "Dark", |
|||
Auto: "Auto", |
|||
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar", |
|||
Normal: "Normal", |
|||
Bottom: "Bottom", |
|||
None: "None", |
|||
Timezone: "Timezone", |
|||
"Search Engine Visibility": "Search Engine Visibility", |
|||
"Allow indexing": "Allow indexing", |
|||
"Discourage search engines from indexing site": "Discourage search engines from indexing site", |
|||
"Change Password": "Change Password", |
|||
"Current Password": "Current Password", |
|||
"New Password": "New Password", |
|||
"Repeat New Password": "Repeat New Password", |
|||
"Update Password": "Update Password", |
|||
"Disable Auth": "Disable Auth", |
|||
"Enable Auth": "Enable Auth", |
|||
Logout: "Logout", |
|||
Leave: "Leave", |
|||
"I understand, please disable": "I understand, please disable", |
|||
Confirm: "Confirm", |
|||
Yes: "Yes", |
|||
No: "No", |
|||
Username: "Username", |
|||
Password: "Password", |
|||
"Remember me": "Remember me", |
|||
Login: "Login", |
|||
"No Monitors, please": "No Monitors, please", |
|||
"add one": "add one", |
|||
"Notification Type": "Notification Type", |
|||
Email: "Email", |
|||
Test: "Test", |
|||
"Certificate Info": "Certificate Info", |
|||
"Resolver Server": "Resolver Server", |
|||
"Resource Record Type": "Resource Record Type", |
|||
"Last Result": "Last Result", |
|||
"Create your admin account": "Create your admin account", |
|||
"Repeat Password": "Repeat Password", |
|||
"Import/Export Backup": "Import/Export Backup", |
|||
Export: "Export", |
|||
Import: "Import", |
|||
respTime: "Resp. Time (ms)", |
|||
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" |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue