commit
08c7a37052
@ -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,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,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.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
FROM node:14-alpine3.12 AS release
|
FROM node:14-buster-slim AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
# 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 && \
|
ln -s /usr/bin/python3 /usr/bin/python && \
|
||||||
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \
|
npm install mapbox/node-sqlite3#593c9d --build-from-source
|
||||||
apk del .build-deps
|
|
||||||
|
|
||||||
# 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
|
FROM node:14-bullseye-slim AS release
|
||||||
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/
|
WORKDIR /app
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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 . .
|
# Copy app files from build layer
|
||||||
RUN npm install && npm run build && npm prune
|
COPY --from=build /app /app
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
|
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||||
CMD ["npm", "run", "start-server"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
FROM release AS nightly
|
FROM release AS nightly
|
||||||
RUN npm run mark-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 = {
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
host: "localhost",
|
*/
|
||||||
port: "3001",
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
timeout: 2000,
|
|
||||||
|
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}`);
|
let request = client.request(options, (res) => {
|
||||||
if (res.statusCode == 200) {
|
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
|
||||||
|
if (res.statusCode === 200) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
request.on("error", function (err) {
|
request.on("error", function (err) {
|
||||||
console.log("ERROR");
|
console.error("Health Check ERROR");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
request.end();
|
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))
|
|
||||||
}
|
|
||||||
|
|
@ -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
Load Diff
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;
|
$primary: #5cdd8b;
|
||||||
$danger: #DC3545;
|
$danger: #dc3545;
|
||||||
$warning: #f8a306;
|
$warning: #f8a306;
|
||||||
$link-color: #111;
|
$link-color: #111;
|
||||||
$border-radius: 50rem;
|
$border-radius: 50rem;
|
||||||
|
|
||||||
$highlight: #7ce8a4;
|
$highlight: #7ce8a4;
|
||||||
$highlight-white: #e7faec;
|
$highlight-white: #e7faec;
|
||||||
|
|
||||||
|
$dark-font-color: #b1b8c0;
|
||||||
|
$dark-font-color2: #020b05;
|
||||||
|
$dark-bg: #0d1117;
|
||||||
|
$dark-bg2: #070a10;
|
||||||
|
$dark-border-color: #1d2634;
|
||||||
|
|
||||||
|
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
|
||||||
|
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
|
||||||
|
|
||||||
|
$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 { 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 { fa } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||||
|
|
||||||
// Add Free Font Awesome Icons here
|
// Add Free Font Awesome Icons here
|
||||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
// 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 }
|
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 have changed in this diff Show More
Loading…
Reference in new issue