mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 07:26:59 +08:00
Compare commits
8 Commits
2.0.0-beta
...
testcontai
Author | SHA1 | Date | |
---|---|---|---|
|
c247ecdb10 | ||
|
28c5191195 | ||
|
a6a662e751 | ||
|
e97fd40b54 | ||
|
e80a6ef728 | ||
|
e262056512 | ||
|
4bbd9430f3 | ||
|
18d80b254b |
8
.github/workflows/auto-test.yml
vendored
8
.github/workflows/auto-test.yml
vendored
@@ -15,14 +15,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-test:
|
auto-test:
|
||||||
needs: [ check-linters ]
|
needs: [ check-linters, e2e-test ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||||
node: [ 18, 20 ]
|
node: [ 18, 20.5 ]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||||
armv7-simple-test:
|
armv7-simple-test:
|
||||||
needs: [ ]
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
- run: npm run lint:prod
|
- run: npm run lint:prod
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
needs: [ ]
|
needs: [ check-linters ]
|
||||||
runs-on: ARM64
|
runs-on: ARM64
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
name: validate
|
name: json-yaml-validate
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -25,19 +25,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
comment: "true" # enable comment mode
|
comment: "true" # enable comment mode
|
||||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
||||||
|
|
||||||
# General validations
|
|
||||||
validate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Use Node.js 20
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Validate language JSON files
|
|
||||||
run: node ./extra/check-lang-json.js
|
|
||||||
|
|
||||||
- name: Validate knex migrations filename
|
|
||||||
run: node ./extra/check-knex-filenames.mjs
|
|
@@ -1,6 +1,6 @@
|
|||||||
# Project Info
|
# Project Info
|
||||||
|
|
||||||
First of all, I want to thank everyone who has submitted issues or shared pull requests for Uptime Kuma.
|
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
|
||||||
I never thought the GitHub community would be so nice!
|
I never thought the GitHub community would be so nice!
|
||||||
Because of this, I also never thought that other people would actually read and edit my code.
|
Because of this, I also never thought that other people would actually read and edit my code.
|
||||||
Parts of the code are not very well-structured or commented, sorry about that.
|
Parts of the code are not very well-structured or commented, sorry about that.
|
||||||
@@ -9,7 +9,7 @@ The project was created with `vite.js` and is written in `vue3`.
|
|||||||
Our backend lives in the `server`-directory and mostly communicates via websockets.
|
Our backend lives in the `server`-directory and mostly communicates via websockets.
|
||||||
Both frontend and backend share the same `package.json`.
|
Both frontend and backend share the same `package.json`.
|
||||||
|
|
||||||
For production, the frontend is built into the `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
|
For production, the frontend is build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
|
||||||
For development, we run vite in development mode on another port.
|
For development, we run vite in development mode on another port.
|
||||||
|
|
||||||
## Directories
|
## Directories
|
||||||
@@ -28,7 +28,7 @@ For development, we run vite in development mode on another port.
|
|||||||
## Can I create a pull request for Uptime Kuma?
|
## Can I create a pull request for Uptime Kuma?
|
||||||
|
|
||||||
Yes or no, it depends on what you will try to do.
|
Yes or no, it depends on what you will try to do.
|
||||||
Both yours and our maintainers' time is precious, and we don't want to waste either.
|
Both your and our maintainers time is precious, and we don't want to waste both time.
|
||||||
|
|
||||||
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
|
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
|
||||||
|
|
||||||
@@ -49,11 +49,11 @@ Different guidelines exist for different types of pull requests (PRs):
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
If you come across a bug and think you can solve, we appreciate your work.
|
If you come across a bug and think you can solve, we appreciate your work.
|
||||||
Please make sure that you follow these rules:
|
Please make sure that you follow by these rules:
|
||||||
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
|
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
|
||||||
- test that your code does what you claim it does.
|
- test that your code does what you came it does.
|
||||||
|
|
||||||
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
|
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
|
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
|
||||||
@@ -68,7 +68,7 @@ Different guidelines exist for different types of pull requests (PRs):
|
|||||||
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
|
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
|
||||||
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
|
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
|
||||||
|
|
||||||
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
|
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
- <details><summary><b>new notification providers</b></summary>
|
- <details><summary><b>new notification providers</b></summary>
|
||||||
@@ -102,7 +102,7 @@ Different guidelines exist for different types of pull requests (PRs):
|
|||||||
Therefore, making sure that they work is also really important.
|
Therefore, making sure that they work is also really important.
|
||||||
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
|
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
|
||||||
|
|
||||||
To make sure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
|
To make shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
|
||||||
- `UP`/`DOWN`
|
- `UP`/`DOWN`
|
||||||
- Certificate Expiry via https://expired.badssl.com/
|
- Certificate Expiry via https://expired.badssl.com/
|
||||||
- Testing (the test button on the notification provider setup page)
|
- Testing (the test button on the notification provider setup page)
|
||||||
@@ -117,7 +117,7 @@ Different guidelines exist for different types of pull requests (PRs):
|
|||||||
| Testing | paste-image-here | paste-image-here |
|
| Testing | paste-image-here | paste-image-here |
|
||||||
```
|
```
|
||||||
|
|
||||||
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
|
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
- <details><summary><b>new monitoring types</b></summary>
|
- <details><summary><b>new monitoring types</b></summary>
|
||||||
@@ -138,14 +138,14 @@ Different guidelines exist for different types of pull requests (PRs):
|
|||||||
-
|
-
|
||||||
|
|
||||||
|
|
||||||
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
|
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
|
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
|
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
|
||||||
This is especially important for a large pull request or when you don't know if it will be merged or not.
|
This is especially important for a large pull request or you don't know if it will be merged or not.
|
||||||
|
|
||||||
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
|
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
|
||||||
</p>
|
</p>
|
||||||
@@ -201,7 +201,7 @@ The rationale behind this is that we can align the direction and scope of the fe
|
|||||||
|
|
||||||
## Project Styles
|
## Project Styles
|
||||||
|
|
||||||
I personally do not like something that requires a lot of configuration before you can finally start the app.
|
I personally do not like something that requires so many configurations before you can finally start the app.
|
||||||
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
|
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
|
||||||
|
|
||||||
- Easy to install for non-Docker users
|
- Easy to install for non-Docker users
|
||||||
@@ -260,7 +260,7 @@ Port `3000` and port `3001` will be used.
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
|
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-frontend-dev
|
npm run start-frontend-dev
|
||||||
@@ -409,7 +409,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
|||||||
|
|
||||||
### What is a maintainer and what are their roles?
|
### What is a maintainer and what are their roles?
|
||||||
|
|
||||||
This project has multiple maintainers who specialise in different areas.
|
This project has multiple maintainers which specialise in different areas.
|
||||||
Currently, there are 3 maintainers:
|
Currently, there are 3 maintainers:
|
||||||
|
|
||||||
| Person | Role | Main Area |
|
| Person | Role | Main Area |
|
||||||
|
@@ -16,7 +16,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||||
"process.env": {},
|
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
||||||
|
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
||||||
|
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
exports.up = function (knex) {
|
|
||||||
return knex.schema.alterTable("monitor", function (table) {
|
|
||||||
table.text("rabbitmq_nodes");
|
|
||||||
table.string("rabbitmq_username");
|
|
||||||
table.string("rabbitmq_password");
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
return knex.schema.alterTable("monitor", function (table) {
|
|
||||||
table.dropColumn("rabbitmq_nodes");
|
|
||||||
table.dropColumn("rabbitmq_username");
|
|
||||||
table.dropColumn("rabbitmq_password");
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
@@ -1,13 +0,0 @@
|
|||||||
// Update info_json column to LONGTEXT mainly for MariaDB
|
|
||||||
exports.up = function (knex) {
|
|
||||||
return knex.schema
|
|
||||||
.alterTable("monitor_tls_info", function (table) {
|
|
||||||
table.text("info_json", "longtext").alter();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
return knex.schema.alterTable("monitor_tls_info", function (table) {
|
|
||||||
table.text("info_json", "text").alter();
|
|
||||||
});
|
|
||||||
};
|
|
@@ -1,13 +1,3 @@
|
|||||||
# Download Apprise deb package
|
|
||||||
FROM node:20-bookworm-slim AS download-apprise
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
|
|
||||||
RUN apt update && \
|
|
||||||
apt --yes --no-install-recommends install curl && \
|
|
||||||
npm install cheerio semver && \
|
|
||||||
node ./download-apprise.mjs
|
|
||||||
|
|
||||||
# Base Image (Slim)
|
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:20-bookworm-slim AS base2-slim
|
FROM node:20-bookworm-slim AS base2-slim
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
@@ -37,9 +27,8 @@ RUN apt update && \
|
|||||||
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
|
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
|
||||||
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
|
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
|
||||||
# python3-paho-mqtt (#4859)
|
# python3-paho-mqtt (#4859)
|
||||||
# TODO: no idea how to delete the deb file after installation as it becomes a layer already
|
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
|
||||||
COPY --from=download-apprise /app/apprise.deb ./apprise.deb
|
apt update && \
|
||||||
RUN apt update && \
|
|
||||||
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
|
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
rm -f apprise.deb && \
|
rm -f apprise.deb && \
|
||||||
|
@@ -27,6 +27,7 @@ RUN mkdir ./data
|
|||||||
# ⭐ Main Image
|
# ⭐ Main Image
|
||||||
############################################
|
############################################
|
||||||
FROM $BASE_IMAGE AS release
|
FROM $BASE_IMAGE AS release
|
||||||
|
USER node
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/louislam/uptime-kuma"
|
LABEL org.opencontainers.image.source="https://github.com/louislam/uptime-kuma"
|
||||||
@@ -45,7 +46,6 @@ CMD ["node", "server/server.js"]
|
|||||||
# Rootless Image
|
# Rootless Image
|
||||||
############################################
|
############################################
|
||||||
FROM release AS rootless
|
FROM release AS rootless
|
||||||
USER node
|
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
# Mark as Nightly
|
# Mark as Nightly
|
||||||
|
@@ -5,7 +5,7 @@ const util = require("../../src/util");
|
|||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const version = process.env.RELEASE_BETA_VERSION;
|
const version = process.env.VERSION;
|
||||||
|
|
||||||
console.log("Beta Version: " + version);
|
console.log("Beta Version: " + version);
|
||||||
|
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
const dir = "./db/knex_migrations";
|
|
||||||
|
|
||||||
// Get the file list (ending with .js) from the directory
|
|
||||||
const files = fs.readdirSync(dir).filter((file) => file !== "README.md");
|
|
||||||
|
|
||||||
// They are wrong, but they had been merged, so allowed.
|
|
||||||
const exceptionList = [
|
|
||||||
"2024-08-24-000-add-cache-bust.js",
|
|
||||||
"2024-10-1315-rabbitmq-monitor.js",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Correct format: YYYY-MM-DD-HHmm-description.js
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (exceptionList.includes(file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ending with .js
|
|
||||||
if (!file.endsWith(".js")) {
|
|
||||||
console.error(`It should end with .js: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = file.split("-");
|
|
||||||
|
|
||||||
// Should be at least 5 parts
|
|
||||||
if (parts.length < 5) {
|
|
||||||
console.error(`Invalid format: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First part should be a year >= 2024
|
|
||||||
const year = parseInt(parts[0], 10);
|
|
||||||
if (isNaN(year) || year < 2023) {
|
|
||||||
console.error(`Invalid year: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second part should be a month
|
|
||||||
const month = parseInt(parts[1], 10);
|
|
||||||
if (isNaN(month) || month < 1 || month > 12) {
|
|
||||||
console.error(`Invalid month: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third part should be a day
|
|
||||||
const day = parseInt(parts[2], 10);
|
|
||||||
if (isNaN(day) || day < 1 || day > 31) {
|
|
||||||
console.error(`Invalid day: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fourth part should be HHmm
|
|
||||||
const time = parts[3];
|
|
||||||
|
|
||||||
// Check length is 4
|
|
||||||
if (time.length !== 4) {
|
|
||||||
console.error(`Invalid time: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hour = parseInt(time.substring(0, 2), 10);
|
|
||||||
const minute = parseInt(time.substring(2), 10);
|
|
||||||
if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) {
|
|
||||||
console.error(`Invalid time: ${file}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("All knex filenames are correct.");
|
|
@@ -1,27 +0,0 @@
|
|||||||
// For #5231
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
let path = "./src/lang";
|
|
||||||
|
|
||||||
// list directories in the lang directory
|
|
||||||
let jsonFileList = fs.readdirSync(path);
|
|
||||||
|
|
||||||
for (let jsonFile of jsonFileList) {
|
|
||||||
if (!jsonFile.endsWith(".json")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let jsonPath = path + "/" + jsonFile;
|
|
||||||
let originalContent = fs.readFileSync(jsonPath, "utf8");
|
|
||||||
let langData = JSON.parse(originalContent);
|
|
||||||
|
|
||||||
let formattedContent = JSON.stringify(langData, null, 4) + "\n";
|
|
||||||
|
|
||||||
if (originalContent !== formattedContent) {
|
|
||||||
console.error(`File ${jsonFile} is not formatted correctly.`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("All lang json files are formatted correctly.");
|
|
@@ -1,57 +0,0 @@
|
|||||||
// Go to http://ftp.debian.org/debian/pool/main/a/apprise/ using fetch api, where it is a apache directory listing page
|
|
||||||
// Use cheerio to parse the html and get the latest version of Apprise
|
|
||||||
// call curl to download the latest version of Apprise
|
|
||||||
// Target file: the latest version of Apprise, which the format is apprise_{VERSION}_all.deb
|
|
||||||
|
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
import semver from "semver";
|
|
||||||
import * as childProcess from "child_process";
|
|
||||||
|
|
||||||
const baseURL = "http://ftp.debian.org/debian/pool/main/a/apprise/";
|
|
||||||
const response = await fetch(baseURL);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch page of Apprise Debian repository.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
// Get all the links in the page
|
|
||||||
const linkElements = $("a");
|
|
||||||
|
|
||||||
// Filter the links which match apprise_{VERSION}_all.deb
|
|
||||||
const links = [];
|
|
||||||
const pattern = /apprise_(.*?)_all.deb/;
|
|
||||||
|
|
||||||
for (let i = 0; i < linkElements.length; i++) {
|
|
||||||
const link = linkElements[i];
|
|
||||||
if (link.attribs.href.match(pattern) && !link.attribs.href.includes("~")) {
|
|
||||||
links.push({
|
|
||||||
filename: link.attribs.href,
|
|
||||||
version: link.attribs.href.match(pattern)[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(links);
|
|
||||||
|
|
||||||
// semver compare and download
|
|
||||||
let latestLink = {
|
|
||||||
filename: "",
|
|
||||||
version: "0.0.0",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
if (semver.gt(link.version, latestLink.version)) {
|
|
||||||
latestLink = link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadURL = baseURL + latestLink.filename;
|
|
||||||
console.log(`Downloading ${downloadURL}...`);
|
|
||||||
let result = childProcess.spawnSync("curl", [ downloadURL, "--output", "apprise.deb" ]);
|
|
||||||
console.log(result.stdout?.toString());
|
|
||||||
console.error(result.stderr?.toString());
|
|
||||||
process.exit(result.status !== null ? result.status : 1);
|
|
@@ -4,6 +4,7 @@ const tar = require("tar");
|
|||||||
|
|
||||||
const packageJSON = require("../package.json");
|
const packageJSON = require("../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const rmSync = require("./fs-rmSync.js");
|
||||||
const version = packageJSON.version;
|
const version = packageJSON.version;
|
||||||
|
|
||||||
const filename = "dist.tar.gz";
|
const filename = "dist.tar.gz";
|
||||||
@@ -28,9 +29,8 @@ function download(url) {
|
|||||||
if (fs.existsSync("./dist")) {
|
if (fs.existsSync("./dist")) {
|
||||||
|
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true,
|
recursive: true
|
||||||
force: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +43,8 @@ function download(url) {
|
|||||||
|
|
||||||
tarStream.on("close", () => {
|
tarStream.on("close", () => {
|
||||||
if (fs.existsSync("./dist-backup")) {
|
if (fs.existsSync("./dist-backup")) {
|
||||||
fs.rmSync("./dist-backup", {
|
rmSync("./dist-backup", {
|
||||||
recursive: true,
|
recursive: true
|
||||||
force: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("Done");
|
console.log("Done");
|
||||||
|
19
extra/env2arg.js
Normal file
19
extra/env2arg.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
let env = process.env;
|
||||||
|
|
||||||
|
let cmd = process.argv[2];
|
||||||
|
let args = process.argv.slice(3);
|
||||||
|
let replacedArgs = [];
|
||||||
|
|
||||||
|
for (let arg of args) {
|
||||||
|
for (let key in env) {
|
||||||
|
arg = arg.replaceAll(`$${key}`, env[key]);
|
||||||
|
}
|
||||||
|
replacedArgs.push(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = childProcess.spawn(cmd, replacedArgs);
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
child.stderr.pipe(process.stderr);
|
1
extra/exe-builder/.gitignore
vendored
Normal file
1
extra/exe-builder/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
packages/
|
35
extra/exe-builder/App.config
Normal file
35
extra/exe-builder/App.config
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||||
|
</startup>
|
||||||
|
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.1" newVersion="4.1.1.1" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
84
extra/exe-builder/DownloadForm.Designer.cs
generated
Normal file
84
extra/exe-builder/DownloadForm.Designer.cs
generated
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace UptimeKuma {
|
||||||
|
partial class DownloadForm {
|
||||||
|
/// <summary>
|
||||||
|
/// Required designer variable.
|
||||||
|
/// </summary>
|
||||||
|
private IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up any resources being used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||||
|
protected override void Dispose(bool disposing) {
|
||||||
|
if (disposing && (components != null)) {
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required method for Designer support - do not modify
|
||||||
|
/// the contents of this method with the code editor.
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeComponent() {
|
||||||
|
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm));
|
||||||
|
this.progressBar = new System.Windows.Forms.ProgressBar();
|
||||||
|
this.label = new System.Windows.Forms.Label();
|
||||||
|
this.labelData = new System.Windows.Forms.Label();
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// progressBar
|
||||||
|
//
|
||||||
|
this.progressBar.Location = new System.Drawing.Point(12, 12);
|
||||||
|
this.progressBar.Name = "progressBar";
|
||||||
|
this.progressBar.Size = new System.Drawing.Size(472, 41);
|
||||||
|
this.progressBar.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// label
|
||||||
|
//
|
||||||
|
this.label.Location = new System.Drawing.Point(12, 59);
|
||||||
|
this.label.Name = "label";
|
||||||
|
this.label.Size = new System.Drawing.Size(472, 23);
|
||||||
|
this.label.TabIndex = 1;
|
||||||
|
this.label.Text = "Preparing...";
|
||||||
|
//
|
||||||
|
// labelData
|
||||||
|
//
|
||||||
|
this.labelData.Location = new System.Drawing.Point(12, 82);
|
||||||
|
this.labelData.Name = "labelData";
|
||||||
|
this.labelData.Size = new System.Drawing.Size(472, 23);
|
||||||
|
this.labelData.TabIndex = 2;
|
||||||
|
//
|
||||||
|
// DownloadForm
|
||||||
|
//
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.ClientSize = new System.Drawing.Size(496, 117);
|
||||||
|
this.Controls.Add(this.labelData);
|
||||||
|
this.Controls.Add(this.label);
|
||||||
|
this.Controls.Add(this.progressBar);
|
||||||
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||||
|
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||||
|
this.MaximizeBox = false;
|
||||||
|
this.Name = "DownloadForm";
|
||||||
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||||
|
this.Text = "Uptime Kuma";
|
||||||
|
this.Load += new System.EventHandler(this.DownloadForm_Load);
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Windows.Forms.Label labelData;
|
||||||
|
|
||||||
|
private System.Windows.Forms.Label label;
|
||||||
|
|
||||||
|
private System.Windows.Forms.ProgressBar progressBar;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
204
extra/exe-builder/DownloadForm.cs
Normal file
204
extra/exe-builder/DownloadForm.cs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace UptimeKuma {
|
||||||
|
public partial class DownloadForm : Form {
|
||||||
|
private readonly Queue<DownloadItem> downloadQueue = new();
|
||||||
|
private readonly WebClient webClient = new();
|
||||||
|
private DownloadItem currentDownloadItem;
|
||||||
|
|
||||||
|
public DownloadForm() {
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DownloadForm_Load(object sender, EventArgs e) {
|
||||||
|
webClient.DownloadProgressChanged += DownloadProgressChanged;
|
||||||
|
webClient.DownloadFileCompleted += DownloadFileCompleted;
|
||||||
|
|
||||||
|
label.Text = "Reading latest version...";
|
||||||
|
|
||||||
|
// Read json from https://uptime.kuma.pet/version
|
||||||
|
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
||||||
|
var versionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
||||||
|
|
||||||
|
var nodeVersion = versionObj.nodejs;
|
||||||
|
var uptimeKumaVersion = versionObj.latest;
|
||||||
|
var hasUpdateFile = File.Exists("update");
|
||||||
|
|
||||||
|
if (!Directory.Exists("node")) {
|
||||||
|
downloadQueue.Enqueue(new DownloadItem {
|
||||||
|
URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip",
|
||||||
|
Filename = "node.zip",
|
||||||
|
TargetFolder = "node"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists("core") || hasUpdateFile) {
|
||||||
|
|
||||||
|
// It is update, rename the core folder to core.old
|
||||||
|
if (Directory.Exists("core")) {
|
||||||
|
// Remove the old core.old folder
|
||||||
|
if (Directory.Exists("core.old")) {
|
||||||
|
Directory.Delete("core.old", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.Move("core", "core.old");
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadQueue.Enqueue(new DownloadItem {
|
||||||
|
URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip",
|
||||||
|
Filename = "core.zip",
|
||||||
|
TargetFolder = "core"
|
||||||
|
});
|
||||||
|
|
||||||
|
File.WriteAllText("version.json", versionJson);
|
||||||
|
|
||||||
|
// Delete the update file
|
||||||
|
if (hasUpdateFile) {
|
||||||
|
File.Delete("update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadNextFile() {
|
||||||
|
if (downloadQueue.Count > 0) {
|
||||||
|
var item = downloadQueue.Dequeue();
|
||||||
|
|
||||||
|
currentDownloadItem = item;
|
||||||
|
|
||||||
|
// Download if the zip file is not existing
|
||||||
|
if (!File.Exists(item.Filename)) {
|
||||||
|
label.Text = item.URL;
|
||||||
|
webClient.DownloadFileAsync(new Uri(item.URL), item.Filename);
|
||||||
|
} else {
|
||||||
|
progressBar.Value = 100;
|
||||||
|
label.Text = "Use local " + item.Filename;
|
||||||
|
DownloadFileCompleted(null, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
npmSetup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void npmSetup() {
|
||||||
|
labelData.Text = "";
|
||||||
|
|
||||||
|
var npm = "..\\node\\npm.cmd";
|
||||||
|
var cmd = $"{npm} ci --production & {npm} run download-dist & exit";
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo {
|
||||||
|
FileName = "cmd.exe",
|
||||||
|
Arguments = $"/k \"{cmd}\"",
|
||||||
|
RedirectStandardOutput = false,
|
||||||
|
RedirectStandardError = false,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = false,
|
||||||
|
WorkingDirectory = "core"
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = new Process();
|
||||||
|
process.StartInfo = startInfo;
|
||||||
|
process.EnableRaisingEvents = true;
|
||||||
|
process.Exited += (_, e) => {
|
||||||
|
progressBar.Value = 100;
|
||||||
|
|
||||||
|
if (process.ExitCode == 0) {
|
||||||
|
Task.Delay(2000).ContinueWith(_ => {
|
||||||
|
Application.Restart();
|
||||||
|
});
|
||||||
|
label.Text = "Done";
|
||||||
|
} else {
|
||||||
|
label.Text = "Failed, exit code: " + process.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
label.Text = "Installing dependencies and download dist files";
|
||||||
|
progressBar.Value = 50;
|
||||||
|
process.WaitForExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) {
|
||||||
|
progressBar.Value = e.ProgressPercentage;
|
||||||
|
var total = e.TotalBytesToReceive / 1024;
|
||||||
|
var current = e.BytesReceived / 1024;
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
labelData.Text = $"{current}KB/{total}KB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) {
|
||||||
|
Extract(currentDownloadItem);
|
||||||
|
DownloadNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Extract(DownloadItem item) {
|
||||||
|
if (Directory.Exists(item.TargetFolder)) {
|
||||||
|
var dir = new DirectoryInfo(item.TargetFolder);
|
||||||
|
dir.Delete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists("temp")) {
|
||||||
|
var dir = new DirectoryInfo("temp");
|
||||||
|
dir.Delete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
labelData.Text = $"Extracting {item.Filename}...";
|
||||||
|
|
||||||
|
ZipFile.ExtractToDirectory(item.Filename, "temp");
|
||||||
|
|
||||||
|
string[] dirList;
|
||||||
|
|
||||||
|
// Move to the correct level
|
||||||
|
dirList = Directory.GetDirectories("temp");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (dirList.Length > 0) {
|
||||||
|
var dir = dirList[0];
|
||||||
|
|
||||||
|
// As sometime ExtractToDirectory is still locking the directory, loop until ok
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
Directory.Move(dir, item.TargetFolder);
|
||||||
|
break;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
labelData.Text = $"Extracted";
|
||||||
|
|
||||||
|
if (Directory.Exists("temp")) {
|
||||||
|
var dir = new DirectoryInfo("temp");
|
||||||
|
dir.Delete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Delete(item.Filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DownloadItem {
|
||||||
|
public string URL { get; set; }
|
||||||
|
public string Filename { get; set; }
|
||||||
|
public string TargetFolder { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
377
extra/exe-builder/DownloadForm.resx
Normal file
377
extra/exe-builder/DownloadForm.resx
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||||
|
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>
|
||||||
|
AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA
|
||||||
|
AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//
|
||||||
|
/wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r
|
||||||
|
u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw
|
||||||
|
8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq
|
||||||
|
s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t
|
||||||
|
163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA
|
||||||
|
AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp
|
||||||
|
sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr
|
||||||
|
u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw
|
||||||
|
8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo
|
||||||
|
rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr
|
||||||
|
uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv
|
||||||
|
5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq
|
||||||
|
tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp
|
||||||
|
sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s
|
||||||
|
vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp
|
||||||
|
r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr
|
||||||
|
uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv//
|
||||||
|
/wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po
|
||||||
|
rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr
|
||||||
|
t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho
|
||||||
|
qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i
|
||||||
|
dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n
|
||||||
|
qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh
|
||||||
|
b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls
|
||||||
|
w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg
|
||||||
|
av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr
|
||||||
|
uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf
|
||||||
|
ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq
|
||||||
|
t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0
|
||||||
|
9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn
|
||||||
|
of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi
|
||||||
|
dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh
|
||||||
|
bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg
|
||||||
|
af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x
|
||||||
|
74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De
|
||||||
|
Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq
|
||||||
|
tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd
|
||||||
|
Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq
|
||||||
|
s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA
|
||||||
|
AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp
|
||||||
|
sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf
|
||||||
|
Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e
|
||||||
|
Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl
|
||||||
|
mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds
|
||||||
|
wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx
|
||||||
|
8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp
|
||||||
|
sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po
|
||||||
|
rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu
|
||||||
|
2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho
|
||||||
|
q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo
|
||||||
|
6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy
|
||||||
|
8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A
|
||||||
|
AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD
|
||||||
|
AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA
|
||||||
|
AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH
|
||||||
|
AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA
|
||||||
|
AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf//
|
||||||
|
AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA
|
||||||
|
gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0
|
||||||
|
9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj
|
||||||
|
4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv
|
||||||
|
3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy
|
||||||
|
8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq
|
||||||
|
s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp
|
||||||
|
sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo
|
||||||
|
rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s
|
||||||
|
vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp
|
||||||
|
sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr
|
||||||
|
uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx
|
||||||
|
8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq
|
||||||
|
tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx
|
||||||
|
8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi
|
||||||
|
c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz
|
||||||
|
80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh
|
||||||
|
bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf
|
||||||
|
Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e
|
||||||
|
YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv
|
||||||
|
45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq
|
||||||
|
t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n
|
||||||
|
qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq
|
||||||
|
s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp
|
||||||
|
sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk
|
||||||
|
i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0
|
||||||
|
9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm
|
||||||
|
mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||||
|
qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1
|
||||||
|
9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n
|
||||||
|
qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo
|
||||||
|
ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA
|
||||||
|
AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA
|
||||||
|
AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw
|
||||||
|
8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||||
|
8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs
|
||||||
|
xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp
|
||||||
|
sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp
|
||||||
|
rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho
|
||||||
|
qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n
|
||||||
|
qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw
|
||||||
|
6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx
|
||||||
|
8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV
|
||||||
|
1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA
|
||||||
|
AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy
|
||||||
|
8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx
|
||||||
|
8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz
|
||||||
|
80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs
|
||||||
|
7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB
|
||||||
|
AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA=
|
||||||
|
</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
3
extra/exe-builder/FodyWeavers.xml
Normal file
3
extra/exe-builder/FodyWeavers.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||||
|
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
||||||
|
</Weavers>
|
141
extra/exe-builder/FodyWeavers.xsd
Normal file
141
extra/exe-builder/FodyWeavers.xsd
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||||
|
<xs:element name="Weavers">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:all>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:all>
|
||||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:schema>
|
262
extra/exe-builder/Program.cs
Normal file
262
extra/exe-builder/Program.cs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using UptimeKuma.Properties;
|
||||||
|
|
||||||
|
namespace UptimeKuma {
|
||||||
|
static class Program {
|
||||||
|
/// <summary>
|
||||||
|
/// The main entry point for the application.
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
static void Main(string[] args) {
|
||||||
|
var cwd = Path.GetDirectoryName(Application.ExecutablePath);
|
||||||
|
|
||||||
|
if (cwd != null) {
|
||||||
|
Environment.CurrentDirectory = cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIntranet = args.Contains("--intranet");
|
||||||
|
|
||||||
|
if (isIntranet) {
|
||||||
|
Console.WriteLine("The --intranet argument was provided, so we will not try to access the internet. The first time this application runs you'll need to run it without the --intranet param or copy the result from another machine to the intranet server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Application.EnableVisualStyles();
|
||||||
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
Application.Run(new UptimeKumaApplicationContext(isIntranet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UptimeKumaApplicationContext : ApplicationContext
|
||||||
|
{
|
||||||
|
private static Mutex mutex = null;
|
||||||
|
|
||||||
|
const string appName = "Uptime Kuma";
|
||||||
|
|
||||||
|
private NotifyIcon trayIcon;
|
||||||
|
private Process process;
|
||||||
|
|
||||||
|
private MenuItem statusMenuItem;
|
||||||
|
private MenuItem runWhenStarts;
|
||||||
|
private MenuItem openMenuItem;
|
||||||
|
|
||||||
|
private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
|
||||||
|
|
||||||
|
private readonly bool intranetOnly;
|
||||||
|
|
||||||
|
public UptimeKumaApplicationContext(bool intranetOnly) {
|
||||||
|
|
||||||
|
// Single instance only
|
||||||
|
bool createdNew;
|
||||||
|
mutex = new Mutex(true, appName, out createdNew);
|
||||||
|
if (!createdNew) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intranetOnly = intranetOnly;
|
||||||
|
|
||||||
|
var startingText = "Starting server...";
|
||||||
|
trayIcon = new NotifyIcon();
|
||||||
|
trayIcon.Text = startingText;
|
||||||
|
|
||||||
|
runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts);
|
||||||
|
runWhenStarts.Checked = registryKey.GetValue(appName) != null;
|
||||||
|
|
||||||
|
statusMenuItem = new MenuItem(startingText);
|
||||||
|
statusMenuItem.Enabled = false;
|
||||||
|
|
||||||
|
openMenuItem = new MenuItem("Open", Open);
|
||||||
|
openMenuItem.Enabled = false;
|
||||||
|
|
||||||
|
trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location);
|
||||||
|
trayIcon.ContextMenu = new ContextMenu(new MenuItem[] {
|
||||||
|
statusMenuItem,
|
||||||
|
openMenuItem,
|
||||||
|
//new("Debug Console", DebugConsole),
|
||||||
|
runWhenStarts,
|
||||||
|
new("Check for Update...", CheckForUpdate),
|
||||||
|
new("Visit GitHub...", VisitGitHub),
|
||||||
|
new("About", About),
|
||||||
|
new("Exit", Exit),
|
||||||
|
});
|
||||||
|
|
||||||
|
trayIcon.MouseDoubleClick += new MouseEventHandler(Open);
|
||||||
|
trayIcon.Visible = true;
|
||||||
|
|
||||||
|
var hasUpdateFile = File.Exists("update");
|
||||||
|
|
||||||
|
if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) {
|
||||||
|
// Go go go
|
||||||
|
StartProcess();
|
||||||
|
} else {
|
||||||
|
DownloadFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DownloadFiles() {
|
||||||
|
if (intranetOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = new DownloadForm();
|
||||||
|
form.Closed += Exit;
|
||||||
|
form.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunWhenStarts(object sender, EventArgs e) {
|
||||||
|
if (registryKey == null) {
|
||||||
|
MessageBox.Show("Error: Unable to set startup registry key.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runWhenStarts.Checked) {
|
||||||
|
registryKey.DeleteValue(appName, false);
|
||||||
|
runWhenStarts.Checked = false;
|
||||||
|
} else {
|
||||||
|
registryKey.SetValue(appName, Application.ExecutablePath);
|
||||||
|
runWhenStarts.Checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StartProcess() {
|
||||||
|
var startInfo = new ProcessStartInfo {
|
||||||
|
FileName = "node/node.exe",
|
||||||
|
Arguments = "server/server.js --data-dir=\"../data/\"",
|
||||||
|
RedirectStandardOutput = false,
|
||||||
|
RedirectStandardError = false,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WorkingDirectory = "core"
|
||||||
|
};
|
||||||
|
|
||||||
|
process = new Process();
|
||||||
|
process.StartInfo = startInfo;
|
||||||
|
process.EnableRaisingEvents = true;
|
||||||
|
process.Exited += ProcessExited;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.Start();
|
||||||
|
//Open(null, null);
|
||||||
|
|
||||||
|
// Async task to check if the server is ready
|
||||||
|
Task.Run(() => {
|
||||||
|
var runningText = "Server is running";
|
||||||
|
using TcpClient tcpClient = new TcpClient();
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
tcpClient.Connect("127.0.0.1", 3001);
|
||||||
|
statusMenuItem.Text = runningText;
|
||||||
|
openMenuItem.Enabled = true;
|
||||||
|
trayIcon.Text = runningText;
|
||||||
|
break;
|
||||||
|
} catch (Exception) {
|
||||||
|
System.Threading.Thread.Sleep(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopProcess() {
|
||||||
|
process?.Kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Open(object sender, EventArgs e) {
|
||||||
|
Process.Start("http://localhost:3001");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugConsole(object sender, EventArgs e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckForUpdate(object sender, EventArgs e) {
|
||||||
|
if (intranetOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version.json exists
|
||||||
|
if (File.Exists("version.json")) {
|
||||||
|
// Load version.json and compare with the latest version from GitHub
|
||||||
|
var currentVersionObj = JsonConvert.DeserializeObject<Version>(File.ReadAllText("version.json"));
|
||||||
|
|
||||||
|
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
||||||
|
var latestVersionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
||||||
|
|
||||||
|
// Compare version, if the latest version is newer, then update
|
||||||
|
if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) {
|
||||||
|
var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo);
|
||||||
|
if (result == DialogResult.Yes) {
|
||||||
|
// Create a empty file `update`, so the app will download the core files again at startup
|
||||||
|
File.Create("update").Close();
|
||||||
|
|
||||||
|
trayIcon.Visible = false;
|
||||||
|
process?.Kill();
|
||||||
|
|
||||||
|
// Restart the app, it will download the core files again at startup
|
||||||
|
Application.Restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageBox.Show("You are using the latest version.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisitGitHub(object sender, EventArgs e) {
|
||||||
|
if (intranetOnly) {
|
||||||
|
MessageBox.Show("You have parsed in --intranet so we will not try to access the internet or visit github.com, please go to https://github.com/louislam/uptime-kuma if you want to visit github.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start("https://github.com/louislam/uptime-kuma");
|
||||||
|
}
|
||||||
|
|
||||||
|
void About(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Exit(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Hide tray icon, otherwise it will remain shown until user mouses over it
|
||||||
|
trayIcon.Visible = false;
|
||||||
|
process?.Kill();
|
||||||
|
Application.Exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessExited(object sender, EventArgs e) {
|
||||||
|
|
||||||
|
if (process.ExitCode != 0) {
|
||||||
|
var line = "";
|
||||||
|
while (!process.StandardOutput.EndOfStream)
|
||||||
|
{
|
||||||
|
line += process.StandardOutput.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line);
|
||||||
|
}
|
||||||
|
|
||||||
|
trayIcon.Visible = false;
|
||||||
|
Application.Exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
36
extra/exe-builder/Properties/AssemblyInfo.cs
Normal file
36
extra/exe-builder/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||||
|
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Uptime Kuma")]
|
||||||
|
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.2.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.2.0")]
|
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
Normal file
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace UptimeKuma.Properties {
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
|
||||||
|
"4.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class Resources {
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
|
||||||
|
"CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||||
|
.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if ((resourceMan == null)) {
|
||||||
|
global::System.Resources.ResourceManager temp =
|
||||||
|
new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources",
|
||||||
|
typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||||
|
.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get { return resourceCulture; }
|
||||||
|
set { resourceCulture = value; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
extra/exe-builder/Properties/Resources.resx
Normal file
117
extra/exe-builder/Properties/Resources.resx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
</root>
|
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
Normal file
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace UptimeKuma.Properties {
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
|
||||||
|
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||||
|
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||||
|
private static Settings defaultInstance =
|
||||||
|
((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||||
|
|
||||||
|
public static Settings Default {
|
||||||
|
get { return defaultInstance; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
extra/exe-builder/Properties/Settings.settings
Normal file
7
extra/exe-builder/Properties/Settings.settings
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||||
|
<Profiles>
|
||||||
|
<Profile Name="(Default)" />
|
||||||
|
</Profiles>
|
||||||
|
<Settings />
|
||||||
|
</SettingsFile>
|
203
extra/exe-builder/UptimeKuma.csproj
Normal file
203
extra/exe-builder/UptimeKuma.csproj
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}</ProjectGuid>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>UptimeKuma</RootNamespace>
|
||||||
|
<AssemblyName>uptime-kuma</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<ApplicationIcon>..\..\public\favicon.ico</ApplicationIcon>
|
||||||
|
<LangVersion>9</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PostBuildEvent>COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\"</PostBuildEvent>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Win32.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="mscorlib" />
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.AppContext, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.ComponentModel.Composition" />
|
||||||
|
<Reference Include="System.Console, Version=4.0.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Console.4.3.1\lib\net46\System.Console.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Diagnostics.DiagnosticSource, Version=7.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Diagnostics.Tracing, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Globalization.Calendars, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.IO.4.3.0\lib\net462\System.IO.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO.Compression.FileSystem" />
|
||||||
|
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO.FileSystem, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO.FileSystem.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Linq, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Linq.4.3.0\lib\net463\System.Linq.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Linq.Expressions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Net.Http, Version=4.1.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Net.Sockets, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Numerics" />
|
||||||
|
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Reflection, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime.Extensions, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime.InteropServices, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Security.Cryptography.Algorithms, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Security.Cryptography.Encoding, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Security.Cryptography.Primitives, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Security.Cryptography.X509Certificates, Version=4.1.1.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Text.RegularExpressions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Deployment" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="System.Xml.ReaderWriter, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="DownloadForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="DownloadForm.Designer.cs">
|
||||||
|
<DependentUpon>DownloadForm.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Version.cs" />
|
||||||
|
<EmbeddedResource Include="DownloadForm.resx">
|
||||||
|
<DependentUpon>DownloadForm.cs</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Properties\Resources.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<Compile Include="Properties\Resources.Designer.cs">
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<None Include="..\..\public\favicon.ico">
|
||||||
|
<Link>favicon.ico</Link>
|
||||||
|
</None>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
<None Include="Properties\Settings.settings">
|
||||||
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
|
</None>
|
||||||
|
<Compile Include="Properties\Settings.Designer.cs">
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include=".gitignore" />
|
||||||
|
<Content Include="app.manifest" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets'))" />
|
||||||
|
</Target>
|
||||||
|
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
|
||||||
|
</Project>
|
16
extra/exe-builder/UptimeKuma.sln
Normal file
16
extra/exe-builder/UptimeKuma.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
3
extra/exe-builder/UptimeKuma.sln.DotSettings.user
Normal file
3
extra/exe-builder/UptimeKuma.sln.DotSettings.user
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=UptimeKuma_002FProperties_002FResources/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>
|
9
extra/exe-builder/Version.cs
Normal file
9
extra/exe-builder/Version.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace UptimeKuma {
|
||||||
|
public class Version {
|
||||||
|
public string latest { get; set; }
|
||||||
|
public string slow { get; set; }
|
||||||
|
public string beta { get; set; }
|
||||||
|
public string nodejs { get; set; }
|
||||||
|
public string exe { get; set; }
|
||||||
|
}
|
||||||
|
}
|
28
extra/exe-builder/app.manifest
Normal file
28
extra/exe-builder/app.manifest
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- UAC Manifest Options
|
||||||
|
If you want to change the Windows User Account Control level replace the
|
||||||
|
requestedExecutionLevel node with one of the following.
|
||||||
|
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||||
|
|
||||||
|
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||||
|
Remove this element if your application requires this virtualization for backwards
|
||||||
|
compatibility.
|
||||||
|
-->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
</assembly>
|
54
extra/exe-builder/packages.config
Normal file
54
extra/exe-builder/packages.config
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Microsoft.NETCore.Platforms" version="7.0.0" targetFramework="net472" />
|
||||||
|
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
|
||||||
|
<package id="Newtonsoft.Json" version="13.0.2" targetFramework="net472" />
|
||||||
|
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Console" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.DiagnosticSource" version="7.0.1" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
|
||||||
|
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
||||||
|
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
|
||||||
|
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
|
||||||
|
</packages>
|
23
extra/fs-rmSync.js
Normal file
23
extra/fs-rmSync.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
/**
|
||||||
|
* Detect if `fs.rmSync` is available
|
||||||
|
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
|
||||||
|
* or the `recursive` property removing completely in the future Node.js version.
|
||||||
|
* See the link below.
|
||||||
|
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
|
||||||
|
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
|
||||||
|
* @param {fs.PathLike} path Valid types for path values in "fs".
|
||||||
|
* @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const rmSync = (path, options) => {
|
||||||
|
if (typeof fs.rmSync === "function") {
|
||||||
|
if (options.recursive) {
|
||||||
|
options.force = true;
|
||||||
|
}
|
||||||
|
return fs.rmSync(path, options);
|
||||||
|
}
|
||||||
|
return fs.rmdirSync(path, options);
|
||||||
|
};
|
||||||
|
module.exports = rmSync;
|
@@ -15,6 +15,7 @@ if (newVersion) {
|
|||||||
// Process package.json
|
// Process package.json
|
||||||
pkg.version = newVersion;
|
pkg.version = newVersion;
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, 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");
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
// Process README.md
|
// Process README.md
|
||||||
|
6
extra/press-any-key.js
Normal file
6
extra/press-any-key.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on("data", process.exit.bind(process, 0));
|
||||||
|
|
@@ -1,65 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import {
|
|
||||||
ver,
|
|
||||||
buildDist,
|
|
||||||
buildImage,
|
|
||||||
checkDocker,
|
|
||||||
checkTagExists,
|
|
||||||
checkVersionFormat,
|
|
||||||
dryRun,
|
|
||||||
getRepoName,
|
|
||||||
pressAnyKey,
|
|
||||||
execSync, uploadArtifacts,
|
|
||||||
} from "./lib.mjs";
|
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
const repoName = getRepoName();
|
|
||||||
const version = process.env.RELEASE_BETA_VERSION;
|
|
||||||
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
|
|
||||||
|
|
||||||
console.log("RELEASE_BETA_VERSION:", version);
|
|
||||||
|
|
||||||
if (!githubToken) {
|
|
||||||
console.error("GITHUB_TOKEN is required");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the version is a valid semver
|
|
||||||
checkVersionFormat(version);
|
|
||||||
|
|
||||||
// Check if the semver identifier is "beta"
|
|
||||||
const semverIdentifier = semver.prerelease(version);
|
|
||||||
console.log("Semver identifier:", semverIdentifier);
|
|
||||||
if (semverIdentifier[0] !== "beta") {
|
|
||||||
console.error("VERSION should have a semver identifier of 'beta'");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if docker is running
|
|
||||||
checkDocker();
|
|
||||||
|
|
||||||
// Check if the tag exists
|
|
||||||
await checkTagExists(repoName, version);
|
|
||||||
|
|
||||||
// node extra/beta/update-version.js
|
|
||||||
execSync("node ./extra/beta/update-version.js");
|
|
||||||
|
|
||||||
// Build frontend dist
|
|
||||||
buildDist();
|
|
||||||
|
|
||||||
// Build slim image (rootless)
|
|
||||||
buildImage(repoName, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
|
|
||||||
|
|
||||||
// Build full image (rootless)
|
|
||||||
buildImage(repoName, [ "beta-rootless", ver(version, "rootless") ], "rootless");
|
|
||||||
|
|
||||||
// Build slim image
|
|
||||||
buildImage(repoName, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
|
|
||||||
|
|
||||||
// Build full image
|
|
||||||
buildImage(repoName, [ "beta", version ], "release");
|
|
||||||
|
|
||||||
await pressAnyKey();
|
|
||||||
|
|
||||||
// npm run upload-artifacts
|
|
||||||
uploadArtifacts();
|
|
@@ -1,57 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import {
|
|
||||||
ver,
|
|
||||||
buildDist,
|
|
||||||
buildImage,
|
|
||||||
checkDocker,
|
|
||||||
checkTagExists,
|
|
||||||
checkVersionFormat,
|
|
||||||
getRepoName,
|
|
||||||
pressAnyKey, execSync, uploadArtifacts
|
|
||||||
} from "./lib.mjs";
|
|
||||||
|
|
||||||
const repoName = getRepoName();
|
|
||||||
const version = process.env.RELEASE_VERSION;
|
|
||||||
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
|
|
||||||
|
|
||||||
console.log("RELEASE_VERSION:", version);
|
|
||||||
|
|
||||||
if (!githubToken) {
|
|
||||||
console.error("GITHUB_TOKEN is required");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the version is a valid semver
|
|
||||||
checkVersionFormat(version);
|
|
||||||
|
|
||||||
// Check if docker is running
|
|
||||||
checkDocker();
|
|
||||||
|
|
||||||
// Check if the tag exists
|
|
||||||
await checkTagExists(repoName, version);
|
|
||||||
|
|
||||||
// node extra/beta/update-version.js
|
|
||||||
execSync("node extra/update-version.js");
|
|
||||||
|
|
||||||
// Build frontend dist
|
|
||||||
buildDist();
|
|
||||||
|
|
||||||
// Build slim image (rootless)
|
|
||||||
buildImage(repoName, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
|
|
||||||
|
|
||||||
// Build full image (rootless)
|
|
||||||
buildImage(repoName, [ "2-rootless", ver(version, "rootless") ], "rootless");
|
|
||||||
|
|
||||||
// Build slim image
|
|
||||||
buildImage(repoName, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
|
|
||||||
|
|
||||||
// Build full image
|
|
||||||
buildImage(repoName, [ "next", "2", version ], "release");
|
|
||||||
|
|
||||||
await pressAnyKey();
|
|
||||||
|
|
||||||
// npm run upload-artifacts
|
|
||||||
uploadArtifacts();
|
|
||||||
|
|
||||||
// node extra/update-wiki-version.js
|
|
||||||
execSync("node extra/update-wiki-version.js");
|
|
@@ -1,191 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import * as childProcess from "child_process";
|
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
export const dryRun = process.env.RELEASE_DRY_RUN === "1";
|
|
||||||
|
|
||||||
if (dryRun) {
|
|
||||||
console.info("Dry run enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if docker is running
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function checkDocker() {
|
|
||||||
try {
|
|
||||||
childProcess.execSync("docker ps");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Docker is not running. Please start docker and try again.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Docker Hub repository name
|
|
||||||
*/
|
|
||||||
export function getRepoName() {
|
|
||||||
return process.env.RELEASE_REPO_NAME || "louislam/uptime-kuma";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build frontend dist
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function buildDist() {
|
|
||||||
if (!dryRun) {
|
|
||||||
childProcess.execSync("npm run build", { stdio: "inherit" });
|
|
||||||
} else {
|
|
||||||
console.info("[DRY RUN] npm run build");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build docker image and push to Docker Hub
|
|
||||||
* @param {string} repoName Docker Hub repository name
|
|
||||||
* @param {string[]} tags Docker image tags
|
|
||||||
* @param {string} target Dockerfile's target name
|
|
||||||
* @param {string} buildArgs Docker build args
|
|
||||||
* @param {string} dockerfile Path to Dockerfile
|
|
||||||
* @param {string} platform Build platform
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function buildImage(repoName, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") {
|
|
||||||
let args = [
|
|
||||||
"buildx",
|
|
||||||
"build",
|
|
||||||
"-f",
|
|
||||||
dockerfile,
|
|
||||||
"--platform",
|
|
||||||
platform,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add tags
|
|
||||||
for (let tag of tags) {
|
|
||||||
args.push("-t", `${repoName}:${tag}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
args = [
|
|
||||||
...args,
|
|
||||||
"--target",
|
|
||||||
target,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add build args
|
|
||||||
if (buildArgs) {
|
|
||||||
args.push("--build-arg", buildArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
args = [
|
|
||||||
...args,
|
|
||||||
".",
|
|
||||||
"--push",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
childProcess.spawnSync("docker", args, { stdio: "inherit" });
|
|
||||||
} else {
|
|
||||||
console.log(`[DRY RUN] docker ${args.join(" ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the version already exists on Docker Hub
|
|
||||||
* TODO: use semver to compare versions if it is greater than the previous?
|
|
||||||
* @param {string} repoName Docker Hub repository name
|
|
||||||
* @param {string} version Version to check
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export async function checkTagExists(repoName, version) {
|
|
||||||
console.log(`Checking if version ${version} exists on Docker Hub`);
|
|
||||||
|
|
||||||
// Get a list of tags from the Docker Hub repository
|
|
||||||
let tags = [];
|
|
||||||
|
|
||||||
// It is mainly to check my careless mistake that I forgot to update the release version in .env, so `page_size` is set to 100 is enough, I think.
|
|
||||||
const response = await fetch(`https://hub.docker.com/v2/repositories/${repoName}/tags/?page_size=100`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
tags = data.results.map((tag) => tag.name);
|
|
||||||
} else {
|
|
||||||
console.error("Failed to get tags from Docker Hub");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the version already exists
|
|
||||||
if (tags.includes(version)) {
|
|
||||||
console.error(`Version ${version} already exists`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the version format
|
|
||||||
* @param {string} version Version to check
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function checkVersionFormat(version) {
|
|
||||||
if (!version) {
|
|
||||||
console.error("VERSION is required");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the version format, it should be a semver and must be like this: "2.0.0-beta.0"
|
|
||||||
if (!semver.valid(version)) {
|
|
||||||
console.error("VERSION is not a valid semver version");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Press any key to continue
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export function pressAnyKey() {
|
|
||||||
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
process.stdin.resume();
|
|
||||||
return new Promise(resolve => process.stdin.once("data", data => {
|
|
||||||
process.stdin.setRawMode(false);
|
|
||||||
process.stdin.pause();
|
|
||||||
resolve();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append version identifier
|
|
||||||
* @param {string} version Version
|
|
||||||
* @param {string} identifier Identifier
|
|
||||||
* @returns {string} Version with identifier
|
|
||||||
*/
|
|
||||||
export function ver(version, identifier) {
|
|
||||||
const obj = semver.parse(version);
|
|
||||||
|
|
||||||
if (obj.prerelease.length === 0) {
|
|
||||||
obj.prerelease = [ identifier ];
|
|
||||||
} else {
|
|
||||||
obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-");
|
|
||||||
}
|
|
||||||
return obj.format();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload artifacts to GitHub
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function uploadArtifacts() {
|
|
||||||
execSync("npm run upload-artifacts");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a command
|
|
||||||
* @param {string} cmd Command to execute
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function execSync(cmd) {
|
|
||||||
if (!dryRun) {
|
|
||||||
childProcess.execSync(cmd, { stdio: "inherit" });
|
|
||||||
} else {
|
|
||||||
console.info(`[DRY RUN] ${cmd}`);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { buildDist, buildImage, checkDocker, getRepoName } from "./lib.mjs";
|
|
||||||
|
|
||||||
// Docker Hub repository name
|
|
||||||
const repoName = getRepoName();
|
|
||||||
|
|
||||||
// Check if docker is running
|
|
||||||
checkDocker();
|
|
||||||
|
|
||||||
// Build frontend dist (it will build on the host machine, TODO: build on a container?)
|
|
||||||
buildDist();
|
|
||||||
|
|
||||||
// Build full image (rootless)
|
|
||||||
buildImage(repoName, [ "nightly2-rootless" ], "nightly-rootless");
|
|
||||||
|
|
||||||
// Build full image
|
|
||||||
buildImage(repoName, [ "nightly2" ], "nightly");
|
|
@@ -1,25 +0,0 @@
|
|||||||
// For #5231
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
let path = "../src/lang";
|
|
||||||
|
|
||||||
// list directories in the lang directory
|
|
||||||
let jsonFileList = fs.readdirSync(path);
|
|
||||||
|
|
||||||
for (let jsonFile of jsonFileList) {
|
|
||||||
if (!jsonFile.endsWith(".json")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let jsonPath = path + "/" + jsonFile;
|
|
||||||
let langData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
||||||
|
|
||||||
for (let key in langData) {
|
|
||||||
if (langData[key] === "") {
|
|
||||||
delete langData[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(jsonPath, JSON.stringify(langData, null, 4) + "\n");
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
const { R } = require("redbean-node");
|
|
||||||
const Database = require("../server/database");
|
|
||||||
const args = require("args-parser")(process.argv);
|
|
||||||
const { Settings } = require("../server/settings");
|
|
||||||
|
|
||||||
const main = async () => {
|
|
||||||
console.log("Connecting the database");
|
|
||||||
Database.initDataDir(args);
|
|
||||||
await Database.connect(false, false, true);
|
|
||||||
|
|
||||||
console.log("Deleting all data from aggregate tables");
|
|
||||||
await R.exec("DELETE FROM stat_minutely");
|
|
||||||
await R.exec("DELETE FROM stat_hourly");
|
|
||||||
await R.exec("DELETE FROM stat_daily");
|
|
||||||
|
|
||||||
console.log("Resetting the aggregate table state");
|
|
||||||
await Settings.set("migrateAggregateTableState", "");
|
|
||||||
|
|
||||||
await Database.close();
|
|
||||||
console.log("Done");
|
|
||||||
};
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
9
extra/test-docker.js
Normal file
9
extra/test-docker.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Check if docker is running
|
||||||
|
const { exec } = require("child_process");
|
||||||
|
|
||||||
|
exec("docker ps", (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Docker is not running. Please start docker and try again.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
import rmSync from "../fs-rmSync.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy across the required language files
|
* Copy across the required language files
|
||||||
@@ -15,10 +16,7 @@ import util from "util";
|
|||||||
*/
|
*/
|
||||||
function copyFiles(langCode, baseLang) {
|
function copyFiles(langCode, baseLang) {
|
||||||
if (fs.existsSync("./languages")) {
|
if (fs.existsSync("./languages")) {
|
||||||
fs.rmSync("./languages", {
|
rmSync("./languages", { recursive: true });
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
fs.mkdirSync("./languages");
|
fs.mkdirSync("./languages");
|
||||||
|
|
||||||
@@ -95,9 +93,6 @@ console.log("Updating: " + langCode);
|
|||||||
|
|
||||||
copyFiles(langCode, baseLangCode);
|
copyFiles(langCode, baseLangCode);
|
||||||
await updateLanguage(langCode, baseLangCode);
|
await updateLanguage(langCode, baseLangCode);
|
||||||
fs.rmSync("./languages", {
|
rmSync("./languages", { recursive: true });
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Done. Fixing formatting by ESLint...");
|
console.log("Done. Fixing formatting by ESLint...");
|
||||||
|
@@ -5,7 +5,7 @@ const util = require("../src/util");
|
|||||||
|
|
||||||
util.polyfill();
|
util.polyfill();
|
||||||
|
|
||||||
const newVersion = process.env.RELEASE_VERSION;
|
const newVersion = process.env.VERSION;
|
||||||
|
|
||||||
console.log("New Version: " + newVersion);
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
const newVersion = process.env.RELEASE_VERSION;
|
const newVersion = process.env.VERSION;
|
||||||
|
|
||||||
if (!newVersion) {
|
if (!newVersion) {
|
||||||
console.log("Missing version");
|
console.log("Missing version");
|
||||||
|
4251
package-lock.json
generated
4251
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "2.0.0-beta.0",
|
"version": "2.0.0-dev",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -34,13 +34,20 @@
|
|||||||
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
"playwright-show-report": "playwright show-report ./private/playwright-report",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
|
"build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
|
||||||
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
|
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
|
||||||
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
|
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
|
||||||
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
||||||
|
"build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
|
||||||
|
"build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push",
|
||||||
|
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push",
|
||||||
|
"build-docker-slim-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim-rootless -t louislam/uptime-kuma:$VERSION-slim-rootless --target rootless --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
|
||||||
|
"build-docker-full-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-rootless -t louislam/uptime-kuma:$VERSION-rootless --target rootless . --push",
|
||||||
|
"build-docker-nightly-rootless": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2-rootless --target nightly-rootless . --push",
|
||||||
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.23.15 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
@@ -51,9 +58,8 @@
|
|||||||
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
|
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
|
||||||
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
|
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
|
||||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||||
"release-final": "node ./extra/release/final.mjs",
|
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||||
"release-beta": "node ./extra/release/beta.mjs",
|
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||||
"release-nightly": "node ./extra/release/nightly.mjs",
|
|
||||||
"git-remove-tag": "git tag -d",
|
"git-remove-tag": "git tag -d",
|
||||||
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
"build-dist-and-restart": "npm run build && npm run start-server-dev",
|
||||||
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
|
||||||
@@ -63,7 +69,7 @@
|
|||||||
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
|
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
|
||||||
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
||||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
||||||
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
|
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "~1.8.22",
|
"@grpc/grpc-js": "~1.8.22",
|
||||||
@@ -103,7 +109,7 @@
|
|||||||
"jsonwebtoken": "~9.0.0",
|
"jsonwebtoken": "~9.0.0",
|
||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
"kafkajs": "^2.2.4",
|
"kafkajs": "^2.2.4",
|
||||||
"knex": "~3.1.0",
|
"knex": "^2.4.2",
|
||||||
"limiter": "~2.1.0",
|
"limiter": "~2.1.0",
|
||||||
"liquidjs": "^10.7.0",
|
"liquidjs": "^10.7.0",
|
||||||
"marked": "^14.0.0",
|
"marked": "^14.0.0",
|
||||||
@@ -111,7 +117,7 @@
|
|||||||
"mongodb": "~4.17.1",
|
"mongodb": "~4.17.1",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~11.0.0",
|
"mssql": "~11.0.0",
|
||||||
"mysql2": "~3.11.3",
|
"mysql2": "~3.9.6",
|
||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
"net-snmp": "^3.11.2",
|
"net-snmp": "^3.11.2",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
@@ -150,7 +156,6 @@
|
|||||||
"@playwright/test": "~1.39.0",
|
"@playwright/test": "~1.39.0",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"@testcontainers/hivemq": "^10.13.1",
|
"@testcontainers/hivemq": "^10.13.1",
|
||||||
"@testcontainers/rabbitmq": "^10.13.2",
|
|
||||||
"@types/bootstrap": "~5.1.9",
|
"@types/bootstrap": "~5.1.9",
|
||||||
"@types/node": "^20.8.6",
|
"@types/node": "^20.8.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
const basicAuth = require("express-basic-auth");
|
const basicAuth = require("express-basic-auth");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setting } = require("./util-server");
|
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
@@ -139,7 +138,7 @@ exports.basicAuth = async function (req, res, next) {
|
|||||||
challenge: true,
|
challenge: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabledAuth = await setting("disableAuth");
|
const disabledAuth = await Settings.get("disableAuth");
|
||||||
|
|
||||||
if (!disabledAuth) {
|
if (!disabledAuth) {
|
||||||
middleware(req, res, next);
|
middleware(req, res, next);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const { setSetting, setting } = require("./util-server");
|
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const compareVersions = require("compare-versions");
|
const compareVersions = require("compare-versions");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
|
|
||||||
exports.version = require("../package.json").version;
|
exports.version = require("../package.json").version;
|
||||||
exports.latestVersion = null;
|
exports.latestVersion = null;
|
||||||
@@ -14,7 +14,7 @@ let interval;
|
|||||||
|
|
||||||
exports.startInterval = () => {
|
exports.startInterval = () => {
|
||||||
let check = async () => {
|
let check = async () => {
|
||||||
if (await setting("checkUpdate") === false) {
|
if (await Settings.get("checkUpdate") === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ exports.startInterval = () => {
|
|||||||
res.data.slow = "1000.0.0";
|
res.data.slow = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
let checkBeta = await setting("checkBeta");
|
let checkBeta = await Settings.get("checkBeta");
|
||||||
|
|
||||||
if (checkBeta && res.data.beta) {
|
if (checkBeta && res.data.beta) {
|
||||||
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
|
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
|
||||||
@@ -57,7 +57,7 @@ exports.startInterval = () => {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.enableCheckUpdate = async (value) => {
|
exports.enableCheckUpdate = async (value) => {
|
||||||
await setSetting("checkUpdate", value);
|
await Settings.set("checkUpdate", value);
|
||||||
|
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
||||||
|
@@ -6,8 +6,8 @@ const { R } = require("redbean-node");
|
|||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
const server = UptimeKumaServer.getInstance();
|
const server = UptimeKumaServer.getInstance();
|
||||||
const io = server.io;
|
const io = server.io;
|
||||||
const { setting } = require("./util-server");
|
|
||||||
const checkVersion = require("./check-version");
|
const checkVersion = require("./check-version");
|
||||||
|
const { Settings } = require("./settings");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,8 +158,8 @@ async function sendInfo(socket, hideVersion = false) {
|
|||||||
version,
|
version,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
isContainer,
|
isContainer,
|
||||||
|
primaryBaseURL: await Settings.get("primaryBaseURL"),
|
||||||
dbType,
|
dbType,
|
||||||
primaryBaseURL: await setting("primaryBaseURL"),
|
|
||||||
serverTimezone: await server.getTimezone(),
|
serverTimezone: await server.getTimezone(),
|
||||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||||
});
|
});
|
||||||
|
@@ -1,16 +1,11 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setSetting, setting } = require("./util-server");
|
|
||||||
const { log, sleep } = require("../src/util");
|
const { log, sleep } = require("../src/util");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||||
const mysql = require("mysql2/promise");
|
const mysql = require("mysql2/promise");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const { UptimeCalculator } = require("./uptime-calculator");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
|
|
||||||
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database & App Data Folder
|
* Database & App Data Folder
|
||||||
@@ -199,14 +194,6 @@ class Database {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||||
// Patch "mysql2" knex client
|
|
||||||
// Workaround: Tried extending the ColumnCompiler class, but it didn't work for unknown reasons, so I override the function via prototype
|
|
||||||
const { getDialectByNameOrAlias } = require("knex/lib/dialects");
|
|
||||||
const mysql2 = getDialectByNameOrAlias("mysql2");
|
|
||||||
mysql2.prototype.columnCompiler = function () {
|
|
||||||
return new KumaColumnCompiler(this, ...arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
let dbConfig;
|
let dbConfig;
|
||||||
try {
|
try {
|
||||||
@@ -392,11 +379,9 @@ class Database {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the database
|
* Patch the database
|
||||||
* @param {number} port Start the migration server for aggregate tables on this port if provided
|
|
||||||
* @param {string} hostname Start the migration server for aggregate tables on this hostname if provided
|
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async patch(port = undefined, hostname = undefined) {
|
static async patch() {
|
||||||
// Still need to keep this for old versions of Uptime Kuma
|
// Still need to keep this for old versions of Uptime Kuma
|
||||||
if (Database.dbConfig.type === "sqlite") {
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
await this.patchSqlite();
|
await this.patchSqlite();
|
||||||
@@ -406,23 +391,9 @@ class Database {
|
|||||||
// https://knexjs.org/guide/migrations.html
|
// https://knexjs.org/guide/migrations.html
|
||||||
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
|
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
|
||||||
try {
|
try {
|
||||||
// Disable foreign key check for SQLite
|
|
||||||
// Known issue of knex: https://github.com/drizzle-team/drizzle-orm/issues/1813
|
|
||||||
if (Database.dbConfig.type === "sqlite") {
|
|
||||||
await R.exec("PRAGMA foreign_keys = OFF");
|
|
||||||
}
|
|
||||||
|
|
||||||
await R.knex.migrate.latest({
|
await R.knex.migrate.latest({
|
||||||
directory: Database.knexMigrationsPath,
|
directory: Database.knexMigrationsPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable foreign key check for SQLite
|
|
||||||
if (Database.dbConfig.type === "sqlite") {
|
|
||||||
await R.exec("PRAGMA foreign_keys = ON");
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.migrateAggregateTable(port, hostname);
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Allow missing patch files for downgrade or testing pr.
|
// Allow missing patch files for downgrade or testing pr.
|
||||||
if (e.message.includes("the following files are missing:")) {
|
if (e.message.includes("the following files are missing:")) {
|
||||||
@@ -449,7 +420,7 @@ class Database {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
static async patchSqlite() {
|
static async patchSqlite() {
|
||||||
let version = parseInt(await setting("database_version"));
|
let version = parseInt(await Settings.get("database_version"));
|
||||||
|
|
||||||
if (! version) {
|
if (! version) {
|
||||||
version = 0;
|
version = 0;
|
||||||
@@ -474,7 +445,7 @@ class Database {
|
|||||||
log.info("db", `Patching ${sqlFile}`);
|
log.info("db", `Patching ${sqlFile}`);
|
||||||
await Database.importSQLFile(sqlFile);
|
await Database.importSQLFile(sqlFile);
|
||||||
log.info("db", `Patched ${sqlFile}`);
|
log.info("db", `Patched ${sqlFile}`);
|
||||||
await setSetting("database_version", i);
|
await Settings.set("database_version", i);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
@@ -500,7 +471,7 @@ class Database {
|
|||||||
*/
|
*/
|
||||||
static async patchSqlite2() {
|
static async patchSqlite2() {
|
||||||
log.debug("db", "Database Patch 2.0 Process");
|
log.debug("db", "Database Patch 2.0 Process");
|
||||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
let databasePatchedFiles = await Settings.get("databasePatchedFiles");
|
||||||
|
|
||||||
if (! databasePatchedFiles) {
|
if (! databasePatchedFiles) {
|
||||||
databasePatchedFiles = {};
|
databasePatchedFiles = {};
|
||||||
@@ -528,7 +499,7 @@ class Database {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
await Settings.set("databasePatchedFiles", databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -541,27 +512,27 @@ class Database {
|
|||||||
// Fix 1.13.0 empty slug bug
|
// Fix 1.13.0 empty slug bug
|
||||||
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||||
|
|
||||||
let title = await setting("title");
|
let title = await Settings.get("title");
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
console.log("Migrating Status Page");
|
log.info("database", "Migrating Status Page");
|
||||||
|
|
||||||
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||||
|
|
||||||
if (statusPageCheck !== null) {
|
if (statusPageCheck !== null) {
|
||||||
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
log.info("database", "Migrating Status Page - Skip, default slug record is already existing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusPage = R.dispense("status_page");
|
let statusPage = R.dispense("status_page");
|
||||||
statusPage.slug = "default";
|
statusPage.slug = "default";
|
||||||
statusPage.title = title;
|
statusPage.title = title;
|
||||||
statusPage.description = await setting("description");
|
statusPage.description = await Settings.get("description");
|
||||||
statusPage.icon = await setting("icon");
|
statusPage.icon = await Settings.get("icon");
|
||||||
statusPage.theme = await setting("statusPageTheme");
|
statusPage.theme = await Settings.get("statusPageTheme");
|
||||||
statusPage.published = !!await setting("statusPagePublished");
|
statusPage.published = !!await Settings.get("statusPagePublished");
|
||||||
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
statusPage.search_engine_index = !!await Settings.get("searchEngineIndex");
|
||||||
statusPage.show_tags = !!await setting("statusPageTags");
|
statusPage.show_tags = !!await Settings.get("statusPageTags");
|
||||||
statusPage.password = null;
|
statusPage.password = null;
|
||||||
|
|
||||||
if (!statusPage.title) {
|
if (!statusPage.title) {
|
||||||
@@ -589,13 +560,13 @@ class Database {
|
|||||||
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||||
|
|
||||||
// Migrate Entry Page if it is status page
|
// Migrate Entry Page if it is status page
|
||||||
let entryPage = await setting("entryPage");
|
let entryPage = await Settings.get("entryPage");
|
||||||
|
|
||||||
if (entryPage === "statusPage") {
|
if (entryPage === "statusPage") {
|
||||||
await setSetting("entryPage", "statusPage-default", "general");
|
await Settings.set("entryPage", "statusPage-default", "general");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Migrating Status Page - Done");
|
log.info("database", "Migrating Status Page - Done");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -740,173 +711,6 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate the old data in the heartbeat table to the new format (stat_daily, stat_hourly, stat_minutely)
|
|
||||||
* It should be run once while upgrading V1 to V2
|
|
||||||
*
|
|
||||||
* Normally, it should be in transaction, but UptimeCalculator wasn't designed to be in transaction before that.
|
|
||||||
* I don't want to heavily modify the UptimeCalculator, so it is not in transaction.
|
|
||||||
* Run `npm run reset-migrate-aggregate-table-state` to reset, in case the migration is interrupted.
|
|
||||||
* @param {number} port Start the migration server on this port if provided
|
|
||||||
* @param {string} hostname Start the migration server on this hostname if provided
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async migrateAggregateTable(port, hostname = undefined) {
|
|
||||||
log.debug("db", "Enter Migrate Aggregate Table function");
|
|
||||||
|
|
||||||
// Add a setting for 2.0.0-dev users to skip this migration
|
|
||||||
if (process.env.SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE === "1") {
|
|
||||||
log.warn("db", "SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE is set to 1, skipping aggregate table migration forever (for 2.0.0-dev users)");
|
|
||||||
await Settings.set("migrateAggregateTableState", "migrated");
|
|
||||||
}
|
|
||||||
|
|
||||||
let migrateState = await Settings.get("migrateAggregateTableState");
|
|
||||||
|
|
||||||
// Skip if already migrated
|
|
||||||
// If it is migrating, it possibly means the migration was interrupted, or the migration is in progress
|
|
||||||
if (migrateState === "migrated") {
|
|
||||||
log.debug("db", "Migrated aggregate table already, skip");
|
|
||||||
return;
|
|
||||||
} else if (migrateState === "migrating") {
|
|
||||||
log.warn("db", "Aggregate table migration is already in progress, or it was interrupted");
|
|
||||||
throw new Error("Aggregate table migration is already in progress");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start migration server for displaying the migration status
|
|
||||||
* @type {SimpleMigrationServer}
|
|
||||||
*/
|
|
||||||
let migrationServer;
|
|
||||||
let msg;
|
|
||||||
|
|
||||||
if (port) {
|
|
||||||
migrationServer = new SimpleMigrationServer();
|
|
||||||
await migrationServer.start(port, hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("db", "Migrating Aggregate Table");
|
|
||||||
|
|
||||||
log.info("db", "Getting list of unique monitors");
|
|
||||||
|
|
||||||
// Get a list of unique monitors from the heartbeat table, using raw sql
|
|
||||||
let monitors = await R.getAll(`
|
|
||||||
SELECT DISTINCT monitor_id
|
|
||||||
FROM heartbeat
|
|
||||||
ORDER BY monitor_id ASC
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Stop if stat_* tables are not empty
|
|
||||||
for (let table of [ "stat_minutely", "stat_hourly", "stat_daily" ]) {
|
|
||||||
let countResult = await R.getRow(`SELECT COUNT(*) AS count FROM ${table}`);
|
|
||||||
let count = countResult.count;
|
|
||||||
if (count > 0) {
|
|
||||||
log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`);
|
|
||||||
await migrationServer?.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Settings.set("migrateAggregateTableState", "migrating");
|
|
||||||
|
|
||||||
let progressPercent = 0;
|
|
||||||
let part = 100 / monitors.length;
|
|
||||||
let i = 1;
|
|
||||||
for (let monitor of monitors) {
|
|
||||||
// Get a list of unique dates from the heartbeat table, using raw sql
|
|
||||||
let dates = await R.getAll(`
|
|
||||||
SELECT DISTINCT DATE(time) AS date
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
ORDER BY date ASC
|
|
||||||
`, [
|
|
||||||
monitor.monitor_id
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (let date of dates) {
|
|
||||||
// New Uptime Calculator
|
|
||||||
let calculator = new UptimeCalculator();
|
|
||||||
calculator.monitorID = monitor.monitor_id;
|
|
||||||
calculator.setMigrationMode(true);
|
|
||||||
|
|
||||||
// Get all the heartbeats for this monitor and date
|
|
||||||
let heartbeats = await R.getAll(`
|
|
||||||
SELECT status, ping, time
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
AND DATE(time) = ?
|
|
||||||
ORDER BY time ASC
|
|
||||||
`, [ monitor.monitor_id, date.date ]);
|
|
||||||
|
|
||||||
if (heartbeats.length > 0) {
|
|
||||||
msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`;
|
|
||||||
log.info("db", msg);
|
|
||||||
migrationServer?.update(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let heartbeat of heartbeats) {
|
|
||||||
await calculator.update(heartbeat.status, parseFloat(heartbeat.ping), dayjs(heartbeat.time));
|
|
||||||
}
|
|
||||||
|
|
||||||
progressPercent += (Math.round(part / dates.length * 100) / 100);
|
|
||||||
|
|
||||||
// Lazy to fix the floating point issue, it is acceptable since it is just a progress bar
|
|
||||||
if (progressPercent > 100) {
|
|
||||||
progressPercent = 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = "Clearing non-important heartbeats";
|
|
||||||
log.info("db", msg);
|
|
||||||
migrationServer?.update(msg);
|
|
||||||
|
|
||||||
await Database.clearHeartbeatData(true);
|
|
||||||
await Settings.set("migrateAggregateTableState", "migrated");
|
|
||||||
await migrationServer?.stop();
|
|
||||||
|
|
||||||
if (monitors.length > 0) {
|
|
||||||
log.info("db", "Aggregate Table Migration Completed");
|
|
||||||
} else {
|
|
||||||
log.info("db", "No data to migrate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all non-important heartbeats from heartbeat table, keep last 24-hour or {KEEP_LAST_ROWS} rows for each monitor
|
|
||||||
* @param {boolean} detailedLog Log detailed information
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
static async clearHeartbeatData(detailedLog = false) {
|
|
||||||
let monitors = await R.getAll("SELECT id FROM monitor");
|
|
||||||
const sqlHourOffset = Database.sqlHourOffset();
|
|
||||||
|
|
||||||
for (let monitor of monitors) {
|
|
||||||
if (detailedLog) {
|
|
||||||
log.info("db", "Deleting non-important heartbeats for monitor " + monitor.id);
|
|
||||||
}
|
|
||||||
await R.exec(`
|
|
||||||
DELETE FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
AND important = 0
|
|
||||||
AND time < ${sqlHourOffset}
|
|
||||||
AND id NOT IN (
|
|
||||||
SELECT id
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT ?
|
|
||||||
)
|
|
||||||
`, [
|
|
||||||
monitor.id,
|
|
||||||
-24,
|
|
||||||
monitor.id,
|
|
||||||
100,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Database;
|
module.exports = Database;
|
||||||
|
@@ -2,16 +2,15 @@ const { R } = require("redbean-node");
|
|||||||
const { log } = require("../../src/util");
|
const { log } = require("../../src/util");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const { Settings } = require("../settings");
|
const { Settings } = require("../settings");
|
||||||
const dayjs = require("dayjs");
|
|
||||||
|
|
||||||
const DEFAULT_KEEP_PERIOD = 365;
|
const DEFAULT_KEEP_PERIOD = 180;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears old data from the heartbeat table and the stat_daily of the database.
|
* Clears old data from the heartbeat table of the database.
|
||||||
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
|
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const clearOldData = async () => {
|
const clearOldData = async () => {
|
||||||
await Database.clearHeartbeatData();
|
|
||||||
let period = await Settings.get("keepDataPeriodDays");
|
let period = await Settings.get("keepDataPeriodDays");
|
||||||
|
|
||||||
// Set Default Period
|
// Set Default Period
|
||||||
@@ -33,21 +32,16 @@ const clearOldData = async () => {
|
|||||||
if (parsedPeriod < 1) {
|
if (parsedPeriod < 1) {
|
||||||
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
||||||
|
|
||||||
const sqlHourOffset = Database.sqlHourOffset();
|
const sqlHourOffset = Database.sqlHourOffset();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Heartbeat
|
await R.exec(
|
||||||
await R.exec("DELETE FROM heartbeat WHERE time < " + sqlHourOffset, [
|
"DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
|
||||||
parsedPeriod * -24,
|
[ parsedPeriod * -24 ]
|
||||||
]);
|
);
|
||||||
|
|
||||||
let timestamp = dayjs().subtract(parsedPeriod, "day").utc().startOf("day").unix();
|
|
||||||
|
|
||||||
// stat_daily
|
|
||||||
await R.exec("DELETE FROM stat_daily WHERE timestamp < ? ", [
|
|
||||||
timestamp,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (Database.dbConfig.type === "sqlite") {
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
await R.exec("PRAGMA optimize;");
|
await R.exec("PRAGMA optimize;");
|
||||||
@@ -56,8 +50,6 @@ const clearOldData = async () => {
|
|||||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("clearOldData", "Data cleared.");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -4,7 +4,7 @@ const { Prometheus } = require("../prometheus");
|
|||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||||
SQL_DATETIME_FORMAT, evaluateJsonQuery
|
SQL_DATETIME_FORMAT, evaluateJsonQuery
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, mssqlQuery, postgresQuery, mysqlQuery, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
@@ -24,6 +24,7 @@ const { CookieJar } = require("tough-cookie");
|
|||||||
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
const rootCertificates = rootCertificatesFingerprints();
|
const rootCertificates = rootCertificatesFingerprints();
|
||||||
|
|
||||||
@@ -71,12 +72,23 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
|
|
||||||
* @param {boolean} includeSensitiveData Include sensitive data in
|
* @param {boolean} includeSensitiveData Include sensitive data in
|
||||||
* JSON
|
* JSON
|
||||||
* @returns {object} Object ready to parse
|
* @returns {Promise<object>} Object ready to parse
|
||||||
*/
|
*/
|
||||||
toJSON(preloadData = {}, includeSensitiveData = true) {
|
async toJSON(includeSensitiveData = true) {
|
||||||
|
|
||||||
|
let notificationIDList = {};
|
||||||
|
|
||||||
|
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||||
|
this.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of list) {
|
||||||
|
notificationIDList[bean.notification_id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await this.getTags();
|
||||||
|
|
||||||
let screenshot = null;
|
let screenshot = null;
|
||||||
|
|
||||||
@@ -84,7 +96,7 @@ class Monitor extends BeanModel {
|
|||||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = preloadData.paths.get(this.id) || [];
|
const path = await this.getPath();
|
||||||
const pathName = path.join(" / ");
|
const pathName = path.join(" / ");
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
@@ -94,15 +106,15 @@ class Monitor extends BeanModel {
|
|||||||
path,
|
path,
|
||||||
pathName,
|
pathName,
|
||||||
parent: this.parent,
|
parent: this.parent,
|
||||||
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
|
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
|
||||||
url: this.url,
|
url: this.url,
|
||||||
method: this.method,
|
method: this.method,
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
maxretries: this.maxretries,
|
maxretries: this.maxretries,
|
||||||
weight: this.weight,
|
weight: this.weight,
|
||||||
active: preloadData.activeStatus.get(this.id),
|
active: await this.isActive(),
|
||||||
forceInactive: preloadData.forceInactive.get(this.id),
|
forceInactive: !await Monitor.isParentActive(this.id),
|
||||||
type: this.type,
|
type: this.type,
|
||||||
timeout: this.timeout,
|
timeout: this.timeout,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
@@ -122,9 +134,9 @@ class Monitor extends BeanModel {
|
|||||||
docker_container: this.docker_container,
|
docker_container: this.docker_container,
|
||||||
docker_host: this.docker_host,
|
docker_host: this.docker_host,
|
||||||
proxyId: this.proxy_id,
|
proxyId: this.proxy_id,
|
||||||
notificationIDList: preloadData.notifications.get(this.id) || {},
|
notificationIDList,
|
||||||
tags: preloadData.tags.get(this.id) || [],
|
tags: tags,
|
||||||
maintenance: preloadData.maintenanceStatus.get(this.id),
|
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
mqttSuccessMessage: this.mqttSuccessMessage,
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
mqttCheckType: this.mqttCheckType,
|
mqttCheckType: this.mqttCheckType,
|
||||||
@@ -153,7 +165,6 @@ class Monitor extends BeanModel {
|
|||||||
snmpOid: this.snmpOid,
|
snmpOid: this.snmpOid,
|
||||||
jsonPathOperator: this.jsonPathOperator,
|
jsonPathOperator: this.jsonPathOperator,
|
||||||
snmpVersion: this.snmpVersion,
|
snmpVersion: this.snmpVersion,
|
||||||
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
|
||||||
conditions: JSON.parse(this.conditions),
|
conditions: JSON.parse(this.conditions),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,8 +195,6 @@ class Monitor extends BeanModel {
|
|||||||
tlsCert: this.tlsCert,
|
tlsCert: this.tlsCert,
|
||||||
tlsKey: this.tlsKey,
|
tlsKey: this.tlsKey,
|
||||||
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
||||||
rabbitmqUsername: this.rabbitmqUsername,
|
|
||||||
rabbitmqPassword: this.rabbitmqPassword,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +202,16 @@ class Monitor extends BeanModel {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the monitor is active based on itself and its parents
|
||||||
|
* @returns {Promise<boolean>} Is the monitor active?
|
||||||
|
*/
|
||||||
|
async isActive() {
|
||||||
|
const parentActive = await Monitor.isParentActive(this.id);
|
||||||
|
|
||||||
|
return (this.active === 1) && parentActive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all tags applied to this monitor
|
* Get all tags applied to this monitor
|
||||||
* @returns {Promise<LooseObject<any>[]>} List of tags on the
|
* @returns {Promise<LooseObject<any>[]>} List of tags on the
|
||||||
@@ -328,7 +347,7 @@ class Monitor extends BeanModel {
|
|||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
|
||||||
this.prometheus = new Prometheus(this);
|
this.prometheus = await Prometheus.createAndInitMetrics(this);
|
||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
@@ -654,7 +673,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
} else if (this.type === "steam") {
|
} else if (this.type === "steam") {
|
||||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||||
const steamAPIKey = await setting("steamAPIKey");
|
const steamAPIKey = await Settings.get("steamAPIKey");
|
||||||
const filter = `addr\\${this.hostname}:${this.port}`;
|
const filter = `addr\\${this.hostname}:${this.port}`;
|
||||||
|
|
||||||
if (!steamAPIKey) {
|
if (!steamAPIKey) {
|
||||||
@@ -980,7 +999,7 @@ class Monitor extends BeanModel {
|
|||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||||
this.prometheus?.update(bean, tlsInfo);
|
await this.prometheus?.update(bean, tlsInfo);
|
||||||
|
|
||||||
previousBeat = bean;
|
previousBeat = bean;
|
||||||
|
|
||||||
@@ -1178,18 +1197,6 @@ class Monitor extends BeanModel {
|
|||||||
return checkCertificateResult;
|
return checkCertificateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the monitor is active based on itself and its parents
|
|
||||||
* @param {number} monitorID ID of monitor to send
|
|
||||||
* @param {boolean} active is active
|
|
||||||
* @returns {Promise<boolean>} Is the monitor active?
|
|
||||||
*/
|
|
||||||
static async isActive(monitorID, active) {
|
|
||||||
const parentActive = await Monitor.isParentActive(monitorID);
|
|
||||||
|
|
||||||
return (active === 1) && parentActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send statistics to clients
|
* Send statistics to clients
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@@ -1326,10 +1333,7 @@ class Monitor extends BeanModel {
|
|||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
const heartbeatJSON = bean.toJSON();
|
const heartbeatJSON = bean.toJSON();
|
||||||
const monitorData = [{ id: monitor.id,
|
|
||||||
active: monitor.active
|
|
||||||
}];
|
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
|
||||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||||
if (!heartbeatJSON["msg"]) {
|
if (!heartbeatJSON["msg"]) {
|
||||||
heartbeatJSON["msg"] = "N/A";
|
heartbeatJSON["msg"] = "N/A";
|
||||||
@@ -1340,7 +1344,7 @@ class Monitor extends BeanModel {
|
|||||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
||||||
|
|
||||||
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
|
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||||
log.error("monitor", e);
|
log.error("monitor", e);
|
||||||
@@ -1376,11 +1380,12 @@ class Monitor extends BeanModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let notifyDays = await setting("tlsExpiryNotifyDays");
|
let notifyDays = await Settings.get("tlsExpiryNotifyDays");
|
||||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||||
// Reset Default
|
// Reset Default
|
||||||
await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
await Settings.set("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||||
notifyDays = [ 7, 14, 21 ];
|
notifyDays = [ 7, 14, 21 ];
|
||||||
|
await Settings.set("tlsExpiryNotifyDays", notifyDays, "general");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(notifyDays)) {
|
if (Array.isArray(notifyDays)) {
|
||||||
@@ -1502,108 +1507,6 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets monitor notification of multiple monitor
|
|
||||||
* @param {Array} monitorIDs IDs of monitor to get
|
|
||||||
* @returns {Promise<LooseObject<any>>} object
|
|
||||||
*/
|
|
||||||
static async getMonitorNotification(monitorIDs) {
|
|
||||||
return await R.getAll(`
|
|
||||||
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
|
|
||||||
FROM monitor_notification
|
|
||||||
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
|
|
||||||
`, monitorIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets monitor tags of multiple monitor
|
|
||||||
* @param {Array} monitorIDs IDs of monitor to get
|
|
||||||
* @returns {Promise<LooseObject<any>>} object
|
|
||||||
*/
|
|
||||||
static async getMonitorTag(monitorIDs) {
|
|
||||||
return await R.getAll(`
|
|
||||||
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
|
|
||||||
FROM monitor_tag
|
|
||||||
JOIN tag ON monitor_tag.tag_id = tag.id
|
|
||||||
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
|
|
||||||
`, monitorIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* prepare preloaded data for efficient access
|
|
||||||
* @param {Array} monitorData IDs & active field of monitor to get
|
|
||||||
* @returns {Promise<LooseObject<any>>} object
|
|
||||||
*/
|
|
||||||
static async preparePreloadData(monitorData) {
|
|
||||||
|
|
||||||
const notificationsMap = new Map();
|
|
||||||
const tagsMap = new Map();
|
|
||||||
const maintenanceStatusMap = new Map();
|
|
||||||
const childrenIDsMap = new Map();
|
|
||||||
const activeStatusMap = new Map();
|
|
||||||
const forceInactiveMap = new Map();
|
|
||||||
const pathsMap = new Map();
|
|
||||||
|
|
||||||
if (monitorData.length > 0) {
|
|
||||||
const monitorIDs = monitorData.map(monitor => monitor.id);
|
|
||||||
const notifications = await Monitor.getMonitorNotification(monitorIDs);
|
|
||||||
const tags = await Monitor.getMonitorTag(monitorIDs);
|
|
||||||
const maintenanceStatuses = await Promise.all(monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id)));
|
|
||||||
const childrenIDs = await Promise.all(monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id)));
|
|
||||||
const activeStatuses = await Promise.all(monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active)));
|
|
||||||
const forceInactiveStatuses = await Promise.all(monitorData.map(monitor => Monitor.isParentActive(monitor.id)));
|
|
||||||
const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name)));
|
|
||||||
|
|
||||||
notifications.forEach(row => {
|
|
||||||
if (!notificationsMap.has(row.monitor_id)) {
|
|
||||||
notificationsMap.set(row.monitor_id, {});
|
|
||||||
}
|
|
||||||
notificationsMap.get(row.monitor_id)[row.notification_id] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
tags.forEach(row => {
|
|
||||||
if (!tagsMap.has(row.monitor_id)) {
|
|
||||||
tagsMap.set(row.monitor_id, []);
|
|
||||||
}
|
|
||||||
tagsMap.get(row.monitor_id).push({
|
|
||||||
tag_id: row.tag_id,
|
|
||||||
name: row.name,
|
|
||||||
color: row.color
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
monitorData.forEach((monitor, index) => {
|
|
||||||
maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]);
|
|
||||||
});
|
|
||||||
|
|
||||||
monitorData.forEach((monitor, index) => {
|
|
||||||
childrenIDsMap.set(monitor.id, childrenIDs[index]);
|
|
||||||
});
|
|
||||||
|
|
||||||
monitorData.forEach((monitor, index) => {
|
|
||||||
activeStatusMap.set(monitor.id, activeStatuses[index]);
|
|
||||||
});
|
|
||||||
|
|
||||||
monitorData.forEach((monitor, index) => {
|
|
||||||
forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]);
|
|
||||||
});
|
|
||||||
|
|
||||||
monitorData.forEach((monitor, index) => {
|
|
||||||
pathsMap.set(monitor.id, paths[index]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
notifications: notificationsMap,
|
|
||||||
tags: tagsMap,
|
|
||||||
maintenanceStatus: maintenanceStatusMap,
|
|
||||||
childrenIDs: childrenIDsMap,
|
|
||||||
activeStatus: activeStatusMap,
|
|
||||||
forceInactive: forceInactiveMap,
|
|
||||||
paths: pathsMap,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets Parent of the monitor
|
* Gets Parent of the monitor
|
||||||
* @param {number} monitorID ID of monitor to get
|
* @param {number} monitorID ID of monitor to get
|
||||||
@@ -1636,18 +1539,16 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the full path
|
* Gets the full path
|
||||||
* @param {number} monitorID ID of the monitor to get
|
|
||||||
* @param {string} name of the monitor to get
|
|
||||||
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
|
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
|
||||||
*/
|
*/
|
||||||
static async getAllPath(monitorID, name) {
|
async getPath() {
|
||||||
const path = [ name ];
|
const path = [ this.name ];
|
||||||
|
|
||||||
if (this.parent === null) {
|
if (this.parent === null) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent = await Monitor.getParent(monitorID);
|
let parent = await Monitor.getParent(this.id);
|
||||||
while (parent !== null) {
|
while (parent !== null) {
|
||||||
path.unshift(parent.name);
|
path.unshift(parent.name);
|
||||||
parent = await Monitor.getParent(parent.id);
|
parent = await Monitor.getParent(parent.id);
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
let url = require("url");
|
let url = require("url");
|
||||||
let MemoryCache = require("./memory-cache");
|
let MemoryCache = require("./memory-cache");
|
||||||
|
const { log } = require("../../../src/util");
|
||||||
|
|
||||||
let t = {
|
let t = {
|
||||||
ms: 1,
|
ms: 1,
|
||||||
@@ -90,24 +91,6 @@ function ApiCache() {
|
|||||||
instances.push(this);
|
instances.push(this);
|
||||||
this.id = instances.length;
|
this.id = instances.length;
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs a message to the console if the `DEBUG` environment variable is set.
|
|
||||||
* @param {string} a The first argument to log.
|
|
||||||
* @param {string} b The second argument to log.
|
|
||||||
* @param {string} c The third argument to log.
|
|
||||||
* @param {string} d The fourth argument to log, and so on... (optional)
|
|
||||||
*
|
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
|
||||||
function debug(a, b, c, d) {
|
|
||||||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
|
||||||
return arg !== undefined;
|
|
||||||
});
|
|
||||||
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
|
|
||||||
|
|
||||||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given request and response should be logged.
|
* Returns true if the given request and response should be logged.
|
||||||
* @param {Object} request The HTTP request object.
|
* @param {Object} request The HTTP request object.
|
||||||
@@ -146,7 +129,7 @@ function ApiCache() {
|
|||||||
let groupName = req.apicacheGroup;
|
let groupName = req.apicacheGroup;
|
||||||
|
|
||||||
if (groupName) {
|
if (groupName) {
|
||||||
debug("group detected \"" + groupName + "\"");
|
log.debug("apicache", `group detected "${groupName}"`);
|
||||||
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
||||||
group.unshift(key);
|
group.unshift(key);
|
||||||
}
|
}
|
||||||
@@ -212,7 +195,7 @@ function ApiCache() {
|
|||||||
redis.hset(key, "duration", duration);
|
redis.hset(key, "duration", duration);
|
||||||
redis.expire(key, duration / 1000, expireCallback || function () {});
|
redis.expire(key, duration / 1000, expireCallback || function () {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug("[apicache] error in redis.hset()");
|
log.debug("apicache", `error in redis.hset(): ${err}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memCache.add(key, value, duration, expireCallback);
|
memCache.add(key, value, duration, expireCallback);
|
||||||
@@ -320,10 +303,10 @@ function ApiCache() {
|
|||||||
|
|
||||||
// display log entry
|
// display log entry
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
|
log.debug("apicache", `adding cache entry for "${key}" @ ${strDuration} ${logDuration(elapsed)}`);
|
||||||
debug("_apicache.headers: ", res._apicache.headers);
|
log.debug("apicache", `_apicache.headers: ${JSON.stringify(res._apicache.headers)}`);
|
||||||
debug("res.getHeaders(): ", getSafeHeaders(res));
|
log.debug("apicache", `res.getHeaders(): ${JSON.stringify(getSafeHeaders(res))}`);
|
||||||
debug("cacheObject: ", cacheObject);
|
log.debug("apicache", `cacheObject: ${JSON.stringify(cacheObject)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,10 +385,10 @@ function ApiCache() {
|
|||||||
let redis = globalOptions.redisClient;
|
let redis = globalOptions.redisClient;
|
||||||
|
|
||||||
if (group) {
|
if (group) {
|
||||||
debug("clearing group \"" + target + "\"");
|
log.debug("apicache", `clearing group "${target}"`);
|
||||||
|
|
||||||
group.forEach(function (key) {
|
group.forEach(function (key) {
|
||||||
debug("clearing cached entry for \"" + key + "\"");
|
log.debug("apicache", `clearing cached entry for "${key}"`);
|
||||||
clearTimeout(timers[key]);
|
clearTimeout(timers[key]);
|
||||||
delete timers[key];
|
delete timers[key];
|
||||||
if (!globalOptions.redisClient) {
|
if (!globalOptions.redisClient) {
|
||||||
@@ -414,7 +397,7 @@ function ApiCache() {
|
|||||||
try {
|
try {
|
||||||
redis.del(key);
|
redis.del(key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
log.info("apicache", "error in redis.del(\"" + key + "\")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index.all = index.all.filter(doesntMatch(key));
|
index.all = index.all.filter(doesntMatch(key));
|
||||||
@@ -422,7 +405,7 @@ function ApiCache() {
|
|||||||
|
|
||||||
delete index.groups[target];
|
delete index.groups[target];
|
||||||
} else if (target) {
|
} else if (target) {
|
||||||
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
|
log.debug("apicache", `clearing ${isAutomatic ? "expired" : "cached"} entry for "${target}"`);
|
||||||
clearTimeout(timers[target]);
|
clearTimeout(timers[target]);
|
||||||
delete timers[target];
|
delete timers[target];
|
||||||
// clear actual cached entry
|
// clear actual cached entry
|
||||||
@@ -432,7 +415,7 @@ function ApiCache() {
|
|||||||
try {
|
try {
|
||||||
redis.del(target);
|
redis.del(target);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[apicache] error in redis.del(\"" + target + "\")");
|
log.error("apicache", "error in redis.del(\"" + target + "\")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +432,7 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
debug("clearing entire index");
|
log.debug("apicache", "clearing entire index");
|
||||||
|
|
||||||
if (!redis) {
|
if (!redis) {
|
||||||
memCache.clear();
|
memCache.clear();
|
||||||
@@ -461,7 +444,7 @@ function ApiCache() {
|
|||||||
try {
|
try {
|
||||||
redis.del(key);
|
redis.del(key);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
log.error("apicache", `error in redis.del("${key}"): ${err}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -528,7 +511,7 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get index of a group
|
* Get index of a group
|
||||||
* @param {string} group
|
* @param {string} group
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
this.getIndex = function (group) {
|
this.getIndex = function (group) {
|
||||||
@@ -543,9 +526,9 @@ function ApiCache() {
|
|||||||
* Express middleware
|
* Express middleware
|
||||||
* @param {(string|number)} strDuration Duration to cache responses
|
* @param {(string|number)} strDuration Duration to cache responses
|
||||||
* for.
|
* for.
|
||||||
* @param {function(Object, Object):boolean} middlewareToggle
|
* @param {function(Object, Object):boolean} middlewareToggle
|
||||||
* @param {Object} localOptions Options for APICache
|
* @param {Object} localOptions Options for APICache
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||||
let duration = instance.getDuration(strDuration);
|
let duration = instance.getDuration(strDuration);
|
||||||
@@ -752,7 +735,7 @@ function ApiCache() {
|
|||||||
*/
|
*/
|
||||||
let cache = function (req, res, next) {
|
let cache = function (req, res, next) {
|
||||||
function bypass() {
|
function bypass() {
|
||||||
debug("bypass detected, skipping cache.");
|
log.debug("apicache", "bypass detected, skipping cache.");
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +788,7 @@ function ApiCache() {
|
|||||||
// send if cache hit from memory-cache
|
// send if cache hit from memory-cache
|
||||||
if (cached) {
|
if (cached) {
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
|
log.debug("apicache", `sending cached (memory-cache) version of ${key} ${logDuration(elapsed)}`);
|
||||||
|
|
||||||
perf.hit(key);
|
perf.hit(key);
|
||||||
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
||||||
@@ -817,7 +800,7 @@ function ApiCache() {
|
|||||||
redis.hgetall(key, function (err, obj) {
|
redis.hgetall(key, function (err, obj) {
|
||||||
if (!err && obj && obj.response) {
|
if (!err && obj && obj.response) {
|
||||||
let elapsed = new Date() - req.apicacheTimer;
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
debug("sending cached (redis) version of", key, logDuration(elapsed));
|
log.debug("apicache", "sending cached (redis) version of "+ key+" "+ logDuration(elapsed));
|
||||||
|
|
||||||
perf.hit(key);
|
perf.hit(key);
|
||||||
return sendCachedResponse(
|
return sendCachedResponse(
|
||||||
@@ -859,7 +842,7 @@ function ApiCache() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process options
|
* Process options
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
this.options = function (options) {
|
this.options = function (options) {
|
||||||
@@ -873,7 +856,7 @@ function ApiCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (globalOptions.trackPerformance) {
|
if (globalOptions.trackPerformance) {
|
||||||
debug("WARNING: using trackPerformance flag can cause high memory usage!");
|
log.debug("apicache", "WARNING: using trackPerformance flag can cause high memory usage!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@@ -1,67 +0,0 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
|
||||||
const { log, UP, DOWN } = require("../../src/util");
|
|
||||||
const { axiosAbortSignal } = require("../util-server");
|
|
||||||
const axios = require("axios");
|
|
||||||
|
|
||||||
class RabbitMqMonitorType extends MonitorType {
|
|
||||||
name = "rabbitmq";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async check(monitor, heartbeat, server) {
|
|
||||||
let baseUrls = [];
|
|
||||||
try {
|
|
||||||
baseUrls = JSON.parse(monitor.rabbitmqNodes);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid RabbitMQ Nodes");
|
|
||||||
}
|
|
||||||
|
|
||||||
heartbeat.status = DOWN;
|
|
||||||
for (let baseUrl of baseUrls) {
|
|
||||||
try {
|
|
||||||
// Without a trailing slash, path in baseUrl will be removed. https://example.com/api -> https://example.com
|
|
||||||
if ( !baseUrl.endsWith("/") ) {
|
|
||||||
baseUrl += "/";
|
|
||||||
}
|
|
||||||
const options = {
|
|
||||||
// Do not start with slash, it will strip the trailing slash from baseUrl
|
|
||||||
url: new URL("api/health/checks/alarms/", baseUrl).href,
|
|
||||||
method: "get",
|
|
||||||
timeout: monitor.timeout * 1000,
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": "Basic " + Buffer.from(`${monitor.rabbitmqUsername || ""}:${monitor.rabbitmqPassword || ""}`).toString("base64"),
|
|
||||||
},
|
|
||||||
signal: axiosAbortSignal((monitor.timeout + 10) * 1000),
|
|
||||||
// Capture reason for 503 status
|
|
||||||
validateStatus: (status) => status === 200 || status === 503,
|
|
||||||
};
|
|
||||||
log.debug("monitor", `[${monitor.name}] Axios Request: ${JSON.stringify(options)}`);
|
|
||||||
const res = await axios.request(options);
|
|
||||||
log.debug("monitor", `[${monitor.name}] Axios Response: status=${res.status} body=${JSON.stringify(res.data)}`);
|
|
||||||
if (res.status === 200) {
|
|
||||||
heartbeat.status = UP;
|
|
||||||
heartbeat.msg = "OK";
|
|
||||||
break;
|
|
||||||
} else if (res.status === 503) {
|
|
||||||
heartbeat.msg = res.data.reason;
|
|
||||||
} else {
|
|
||||||
heartbeat.msg = `${res.status} - ${res.statusText}`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isCancel(error)) {
|
|
||||||
heartbeat.msg = "Request timed out";
|
|
||||||
log.debug("monitor", `[${monitor.name}] Request timed out`);
|
|
||||||
} else {
|
|
||||||
log.debug("monitor", `[${monitor.name}] Axios Error: ${JSON.stringify(error.message)}`);
|
|
||||||
heartbeat.msg = error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
RabbitMqMonitorType,
|
|
||||||
};
|
|
@@ -63,7 +63,7 @@ if (process.platform === "win32") {
|
|||||||
* @returns {Promise<boolean>} The executable is allowed?
|
* @returns {Promise<boolean>} The executable is allowed?
|
||||||
*/
|
*/
|
||||||
async function isAllowedChromeExecutable(executablePath) {
|
async function isAllowedChromeExecutable(executablePath) {
|
||||||
console.log(config.args);
|
log.info("Chromium", config.args);
|
||||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,8 @@ async function getBrowser() {
|
|||||||
*/
|
*/
|
||||||
async function getRemoteBrowser(remoteBrowserID, userId) {
|
async function getRemoteBrowser(remoteBrowserID, userId) {
|
||||||
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
||||||
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
log.debug("Chromium", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
||||||
|
browser = chromium.connect(remoteBrowser.url);
|
||||||
browser = await chromium.connect(remoteBrowser.url);
|
browser = await chromium.connect(remoteBrowser.url);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +0,0 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const axios = require("axios");
|
|
||||||
|
|
||||||
class Elks extends NotificationProvider {
|
|
||||||
name = "Elks";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
const okMsg = "Sent Successfully.";
|
|
||||||
const url = "https://api.46elks.com/a1/sms";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let data = new URLSearchParams();
|
|
||||||
data.append("from", notification.elksFromNumber);
|
|
||||||
data.append("to", notification.elksToNumber );
|
|
||||||
data.append("message", msg);
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
headers: {
|
|
||||||
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.post(url, data, config);
|
|
||||||
|
|
||||||
return okMsg;
|
|
||||||
} catch (error) {
|
|
||||||
this.throwGeneralAxiosError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Elks;
|
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { setting } = require("../util-server");
|
|
||||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
class AlertNow extends NotificationProvider {
|
class AlertNow extends NotificationProvider {
|
||||||
name = "AlertNow";
|
name = "AlertNow";
|
||||||
@@ -29,7 +29,7 @@ class AlertNow extends NotificationProvider {
|
|||||||
|
|
||||||
textMsg += ` - ${msg}`;
|
textMsg += ` - ${msg}`;
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL && monitorJSON) {
|
if (baseURL && monitorJSON) {
|
||||||
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
}
|
}
|
||||||
|
@@ -87,6 +87,7 @@ class DingDing extends NotificationProvider {
|
|||||||
* @returns {string} Status
|
* @returns {string} Status
|
||||||
*/
|
*/
|
||||||
statusToString(status) {
|
statusToString(status) {
|
||||||
|
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case DOWN:
|
case DOWN:
|
||||||
return "DOWN";
|
return "DOWN";
|
||||||
|
@@ -48,7 +48,7 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: this.extractAddress(monitorJSON),
|
value: this.extractAdress(monitorJSON),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||||
@@ -85,7 +85,7 @@ class Discord extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||||
value: this.extractAddress(monitorJSON),
|
value: this.extractAdress(monitorJSON),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
const { setting } = require("../util-server");
|
const { Settings } = require("../settings");
|
||||||
const successMessage = "Sent Successfully.";
|
const successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
class FlashDuty extends NotificationProvider {
|
class FlashDuty extends NotificationProvider {
|
||||||
@@ -84,7 +84,7 @@ class FlashDuty extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL && monitorInfo) {
|
if (baseURL && monitorInfo) {
|
||||||
options.client = "Uptime Kuma";
|
options.client = "Uptime Kuma";
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { setting } = require("../util-server");
|
|
||||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
class GoogleChat extends NotificationProvider {
|
class GoogleChat extends NotificationProvider {
|
||||||
name = "GoogleChat";
|
name = "GoogleChat";
|
||||||
@@ -45,7 +45,7 @@ class GoogleChat extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add button for monitor link if available
|
// add button for monitor link if available
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL) {
|
if (baseURL) {
|
||||||
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
|
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
|
||||||
sectionWidgets.push({
|
sectionWidgets.push({
|
||||||
|
@@ -24,7 +24,7 @@ class NotificationProvider {
|
|||||||
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||||
* @returns {string} The extracted address based on the monitor type.
|
* @returns {string} The extracted address based on the monitor type.
|
||||||
*/
|
*/
|
||||||
extractAddress(monitorJSON) {
|
extractAdress(monitorJSON) {
|
||||||
if (!monitorJSON) {
|
if (!monitorJSON) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
const { setting } = require("../util-server");
|
const { Settings } = require("../settings");
|
||||||
let successMessage = "Sent Successfully.";
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
class PagerDuty extends NotificationProvider {
|
class PagerDuty extends NotificationProvider {
|
||||||
@@ -95,7 +95,7 @@ class PagerDuty extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL && monitorInfo) {
|
if (baseURL && monitorInfo) {
|
||||||
options.client = "Uptime Kuma";
|
options.client = "Uptime Kuma";
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
const { setting } = require("../util-server");
|
const { Settings } = require("../settings");
|
||||||
let successMessage = "Sent Successfully.";
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
class PagerTree extends NotificationProvider {
|
class PagerTree extends NotificationProvider {
|
||||||
@@ -74,7 +74,7 @@ class PagerTree extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL && monitorJSON) {
|
if (baseURL && monitorJSON) {
|
||||||
options.client = "Uptime Kuma";
|
options.client = "Uptime Kuma";
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const Slack = require("./slack");
|
const Slack = require("./slack");
|
||||||
const { setting } = require("../util-server");
|
|
||||||
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
|
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
class RocketChat extends NotificationProvider {
|
class RocketChat extends NotificationProvider {
|
||||||
name = "rocket.chat";
|
name = "rocket.chat";
|
||||||
@@ -49,7 +49,7 @@ class RocketChat extends NotificationProvider {
|
|||||||
await Slack.deprecateURL(notification.rocketbutton);
|
await Slack.deprecateURL(notification.rocketbutton);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
|
|
||||||
if (baseURL) {
|
if (baseURL) {
|
||||||
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||||
|
@@ -1,65 +0,0 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
|
||||||
const axios = require("axios");
|
|
||||||
|
|
||||||
class SendGrid extends NotificationProvider {
|
|
||||||
name = "SendGrid";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
|
||||||
const okMsg = "Sent Successfully.";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let config = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${notification.sendgridApiKey}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let personalizations = {
|
|
||||||
to: [{ email: notification.sendgridToEmail }],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add CC recipients if provided
|
|
||||||
if (notification.sendgridCcEmail) {
|
|
||||||
personalizations.cc = notification.sendgridCcEmail
|
|
||||||
.split(",")
|
|
||||||
.map((email) => ({ email: email.trim() }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add BCC recipients if provided
|
|
||||||
if (notification.sendgridBccEmail) {
|
|
||||||
personalizations.bcc = notification.sendgridBccEmail
|
|
||||||
.split(",")
|
|
||||||
.map((email) => ({ email: email.trim() }));
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
personalizations: [ personalizations ],
|
|
||||||
from: { email: notification.sendgridFromEmail.trim() },
|
|
||||||
subject:
|
|
||||||
notification.sendgridSubject ||
|
|
||||||
"Notification from Your Uptime Kuma",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text/plain",
|
|
||||||
value: msg,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.post(
|
|
||||||
"https://api.sendgrid.com/v3/mail/send",
|
|
||||||
data,
|
|
||||||
config
|
|
||||||
);
|
|
||||||
return okMsg;
|
|
||||||
} catch (error) {
|
|
||||||
this.throwGeneralAxiosError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SendGrid;
|
|
@@ -12,9 +12,8 @@ class ServerChan extends NotificationProvider {
|
|||||||
const okMsg = "Sent Successfully.";
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
// serverchan3 requires sending via ft07.com
|
// serverchan3 requires sending via ft07.com
|
||||||
const matchResult = String(notification.serverChanSendKey).match(/^sctp(\d+)t/i);
|
const url = String(notification.serverChanSendKey).startsWith("sctp")
|
||||||
const url = matchResult && matchResult[1]
|
? `https://${notification.serverChanSendKey}.push.ft07.com/send`
|
||||||
? `https://${matchResult[1]}.push.ft07.com/send/${notification.serverChanSendKey}.send`
|
|
||||||
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
|
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -32,7 +32,7 @@ class SevenIO extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
let address = this.extractAddress(monitorJSON);
|
let address = this.extractAdress(monitorJSON);
|
||||||
if (address !== "") {
|
if (address !== "") {
|
||||||
address = `(${address}) `;
|
address = `(${address}) `;
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ class SIGNL4 extends NotificationProvider {
|
|||||||
msg,
|
msg,
|
||||||
// Source system
|
// Source system
|
||||||
"X-S4-SourceSystem": "UptimeKuma",
|
"X-S4-SourceSystem": "UptimeKuma",
|
||||||
monitorUrl: this.extractAddress(monitorJSON),
|
monitorUrl: this.extractAdress(monitorJSON),
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { setSettings, setting } = require("../util-server");
|
|
||||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
|
||||||
class Slack extends NotificationProvider {
|
class Slack extends NotificationProvider {
|
||||||
name = "slack";
|
name = "slack";
|
||||||
@@ -14,15 +15,13 @@ class Slack extends NotificationProvider {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async deprecateURL(url) {
|
static async deprecateURL(url) {
|
||||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
let currentPrimaryBaseURL = await Settings.get("primaryBaseURL");
|
||||||
|
|
||||||
if (!currentPrimaryBaseURL) {
|
if (!currentPrimaryBaseURL) {
|
||||||
console.log("Move the url to be the primary base URL");
|
log.error("notification", "Move the url to be the primary base URL");
|
||||||
await setSettings("general", {
|
await Settings.set("primaryBaseURL", url, "general");
|
||||||
primaryBaseURL: url,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Already there, no need to move the primary base URL");
|
log.debug("notification", "Already there, no need to move the primary base URL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ class Slack extends NotificationProvider {
|
|||||||
* @param {object} monitorJSON The monitor config
|
* @param {object} monitorJSON The monitor config
|
||||||
* @returns {Array} The relevant action objects
|
* @returns {Array} The relevant action objects
|
||||||
*/
|
*/
|
||||||
buildActions(baseURL, monitorJSON) {
|
static buildActions(baseURL, monitorJSON) {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
if (baseURL) {
|
if (baseURL) {
|
||||||
@@ -48,7 +47,7 @@ class Slack extends NotificationProvider {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = this.extractAddress(monitorJSON);
|
const address = this.extractAdress(monitorJSON);
|
||||||
if (address) {
|
if (address) {
|
||||||
actions.push({
|
actions.push({
|
||||||
"type": "button",
|
"type": "button",
|
||||||
@@ -73,7 +72,7 @@ class Slack extends NotificationProvider {
|
|||||||
* @param {string} msg The message body
|
* @param {string} msg The message body
|
||||||
* @returns {Array<object>} The rich content blocks for the Slack message
|
* @returns {Array<object>} The rich content blocks for the Slack message
|
||||||
*/
|
*/
|
||||||
buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
|
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
|
||||||
|
|
||||||
//create an array to dynamically add blocks
|
//create an array to dynamically add blocks
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
@@ -136,26 +135,21 @@ class Slack extends NotificationProvider {
|
|||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
|
|
||||||
const title = "Uptime Kuma Alert";
|
const title = "Uptime Kuma Alert";
|
||||||
let data = {
|
let data = {
|
||||||
|
"text": `${title}\n${msg}`,
|
||||||
"channel": notification.slackchannel,
|
"channel": notification.slackchannel,
|
||||||
"username": notification.slackusername,
|
"username": notification.slackusername,
|
||||||
"icon_emoji": notification.slackiconemo,
|
"icon_emoji": notification.slackiconemo,
|
||||||
"attachments": [],
|
"attachments": [
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.slackrichmessage) {
|
|
||||||
data.attachments.push(
|
|
||||||
{
|
{
|
||||||
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
|
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
|
||||||
"blocks": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
|
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
|
||||||
}
|
}
|
||||||
);
|
]
|
||||||
} else {
|
};
|
||||||
data.text = `${title}\n${msg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.slackbutton) {
|
if (notification.slackbutton) {
|
||||||
await Slack.deprecateURL(notification.slackbutton);
|
await Slack.deprecateURL(notification.slackbutton);
|
||||||
|
@@ -93,7 +93,7 @@ class SMTP extends NotificationProvider {
|
|||||||
|
|
||||||
if (monitorJSON !== null) {
|
if (monitorJSON !== null) {
|
||||||
monitorName = monitorJSON["name"];
|
monitorName = monitorJSON["name"];
|
||||||
monitorHostnameOrURL = this.extractAddress(monitorJSON);
|
monitorHostnameOrURL = this.extractAdress(monitorJSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
let serviceStatus = "⚠️ Test";
|
let serviceStatus = "⚠️ Test";
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
const { setting } = require("../util-server");
|
const { Settings } = require("../settings");
|
||||||
let successMessage = "Sent Successfully.";
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
class Splunk extends NotificationProvider {
|
class Splunk extends NotificationProvider {
|
||||||
@@ -95,7 +95,7 @@ class Splunk extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL && monitorInfo) {
|
if (baseURL && monitorInfo) {
|
||||||
options.client = "Uptime Kuma";
|
options.client = "Uptime Kuma";
|
||||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
@@ -34,7 +34,7 @@ class Squadcast extends NotificationProvider {
|
|||||||
data.status = "resolve";
|
data.status = "resolve";
|
||||||
}
|
}
|
||||||
|
|
||||||
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
|
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
|
||||||
|
|
||||||
monitorJSON["tags"].forEach(tag => {
|
monitorJSON["tags"].forEach(tag => {
|
||||||
data.tags[tag["name"]] = {
|
data.tags[tag["name"]] = {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { setting } = require("../util-server");
|
|
||||||
const { getMonitorRelativeURL } = require("../../src/util");
|
const { getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
class Stackfield extends NotificationProvider {
|
class Stackfield extends NotificationProvider {
|
||||||
name = "stackfield";
|
name = "stackfield";
|
||||||
@@ -23,7 +23,7 @@ class Stackfield extends NotificationProvider {
|
|||||||
|
|
||||||
textMsg += `\n${msg}`;
|
textMsg += `\n${msg}`;
|
||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await Settings.get("primaryBaseURL");
|
||||||
if (baseURL) {
|
if (baseURL) {
|
||||||
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||||
}
|
}
|
||||||
|
@@ -225,7 +225,7 @@ class Teams extends NotificationProvider {
|
|||||||
const payload = this._notificationPayloadFactory({
|
const payload = this._notificationPayloadFactory({
|
||||||
heartbeatJSON: heartbeatJSON,
|
heartbeatJSON: heartbeatJSON,
|
||||||
monitorName: monitorJSON.name,
|
monitorName: monitorJSON.name,
|
||||||
monitorUrl: this.extractAddress(monitorJSON),
|
monitorUrl: this.extractAdress(monitorJSON),
|
||||||
dashboardUrl: dashboardUrl,
|
dashboardUrl: dashboardUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -10,22 +10,11 @@ class TechulusPush extends NotificationProvider {
|
|||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
const okMsg = "Sent Successfully.";
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
let data = {
|
|
||||||
"title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma",
|
|
||||||
"body": msg,
|
|
||||||
"timeSensitive": notification.pushTimeSensitive ?? true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.pushChannel) {
|
|
||||||
data.channel = notification.pushChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification.pushSound) {
|
|
||||||
data.sound = notification.pushSound;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
|
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
|
||||||
|
"title": "Uptime-Kuma",
|
||||||
|
"body": msg,
|
||||||
|
});
|
||||||
return okMsg;
|
return okMsg;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.throwGeneralAxiosError(error);
|
this.throwGeneralAxiosError(error);
|
||||||
|
@@ -32,17 +32,20 @@ class WeCom extends NotificationProvider {
|
|||||||
* @returns {object} Message
|
* @returns {object} Message
|
||||||
*/
|
*/
|
||||||
composeMessage(heartbeatJSON, msg) {
|
composeMessage(heartbeatJSON, msg) {
|
||||||
let title = "UptimeKuma Message";
|
let title;
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||||
title = "UptimeKuma Monitor Up";
|
title = "UptimeKuma Monitor Up";
|
||||||
}
|
}
|
||||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||||
title = "UptimeKuma Monitor Down";
|
title = "UptimeKuma Monitor Down";
|
||||||
}
|
}
|
||||||
|
if (msg != null) {
|
||||||
|
title = "UptimeKuma Message";
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
msgtype: "text",
|
msgtype: "text",
|
||||||
text: {
|
text: {
|
||||||
content: title + "\n" + msg
|
content: title + msg
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -85,7 +85,7 @@ class ZohoCliq extends NotificationProvider {
|
|||||||
const payload = this._notificationPayloadFactory({
|
const payload = this._notificationPayloadFactory({
|
||||||
monitorMessage: heartbeatJSON.msg,
|
monitorMessage: heartbeatJSON.msg,
|
||||||
monitorName: monitorJSON.name,
|
monitorName: monitorJSON.name,
|
||||||
monitorUrl: this.extractAddress(monitorJSON),
|
monitorUrl: this.extractAdress(monitorJSON),
|
||||||
status: heartbeatJSON.status
|
status: heartbeatJSON.status
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -11,7 +11,6 @@ const CallMeBot = require("./notification-providers/call-me-bot");
|
|||||||
const SMSC = require("./notification-providers/smsc");
|
const SMSC = require("./notification-providers/smsc");
|
||||||
const DingDing = require("./notification-providers/dingding");
|
const DingDing = require("./notification-providers/dingding");
|
||||||
const Discord = require("./notification-providers/discord");
|
const Discord = require("./notification-providers/discord");
|
||||||
const Elks = require("./notification-providers/46elks");
|
|
||||||
const Feishu = require("./notification-providers/feishu");
|
const Feishu = require("./notification-providers/feishu");
|
||||||
const FreeMobile = require("./notification-providers/freemobile");
|
const FreeMobile = require("./notification-providers/freemobile");
|
||||||
const GoogleChat = require("./notification-providers/google-chat");
|
const GoogleChat = require("./notification-providers/google-chat");
|
||||||
@@ -68,7 +67,6 @@ const GtxMessaging = require("./notification-providers/gtx-messaging");
|
|||||||
const Cellsynt = require("./notification-providers/cellsynt");
|
const Cellsynt = require("./notification-providers/cellsynt");
|
||||||
const Onesender = require("./notification-providers/onesender");
|
const Onesender = require("./notification-providers/onesender");
|
||||||
const Wpush = require("./notification-providers/wpush");
|
const Wpush = require("./notification-providers/wpush");
|
||||||
const SendGrid = require("./notification-providers/send-grid");
|
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
|
|
||||||
@@ -97,7 +95,6 @@ class Notification {
|
|||||||
new SMSC(),
|
new SMSC(),
|
||||||
new DingDing(),
|
new DingDing(),
|
||||||
new Discord(),
|
new Discord(),
|
||||||
new Elks(),
|
|
||||||
new Feishu(),
|
new Feishu(),
|
||||||
new FreeMobile(),
|
new FreeMobile(),
|
||||||
new GoogleChat(),
|
new GoogleChat(),
|
||||||
@@ -154,7 +151,6 @@ class Notification {
|
|||||||
new GtxMessaging(),
|
new GtxMessaging(),
|
||||||
new Cellsynt(),
|
new Cellsynt(),
|
||||||
new Wpush(),
|
new Wpush(),
|
||||||
new SendGrid()
|
|
||||||
];
|
];
|
||||||
for (let item of list) {
|
for (let item of list) {
|
||||||
if (! item.name) {
|
if (! item.name) {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
const PrometheusClient = require("prom-client");
|
const PrometheusClient = require("prom-client");
|
||||||
const { log } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
|
|
||||||
@@ -9,36 +10,102 @@ const commonLabels = [
|
|||||||
"monitor_port",
|
"monitor_port",
|
||||||
];
|
];
|
||||||
|
|
||||||
const monitorCertDaysRemaining = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_cert_days_remaining",
|
|
||||||
help: "The number of days remaining until the certificate expires",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
const monitorCertIsValid = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_cert_is_valid",
|
|
||||||
help: "Is the certificate still valid? (1 = Yes, 0= No)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
const monitorResponseTime = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_response_time",
|
|
||||||
help: "Monitor Response Time (ms)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
const monitorStatus = new PrometheusClient.Gauge({
|
|
||||||
name: "monitor_status",
|
|
||||||
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
|
|
||||||
labelNames: commonLabels
|
|
||||||
});
|
|
||||||
|
|
||||||
class Prometheus {
|
class Prometheus {
|
||||||
monitorLabelValues = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} monitor Monitor object to monitor
|
* Metric: monitor_cert_days_remaining
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
*/
|
*/
|
||||||
constructor(monitor) {
|
static monitorCertDaysRemaining = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_cert_is_valid
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorCertIsValid = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_response_time
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorResponseTime = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metric: monitor_status
|
||||||
|
* @type {PrometheusClient.Gauge<string> | null}
|
||||||
|
*/
|
||||||
|
static monitorStatus = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All registered metric labels.
|
||||||
|
* @type {string[] | null}
|
||||||
|
*/
|
||||||
|
static monitorLabelNames = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitor labels/values combination.
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
monitorLabelValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize metrics and get all label names the first time called.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
static async initMetrics() {
|
||||||
|
if (!this.monitorLabelNames) {
|
||||||
|
let labelNames = await R.getCol("SELECT name FROM tag");
|
||||||
|
this.monitorLabelNames = [ ...commonLabels, ...labelNames ];
|
||||||
|
}
|
||||||
|
if (!this.monitorCertDaysRemaining) {
|
||||||
|
this.monitorCertDaysRemaining = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_cert_days_remaining",
|
||||||
|
help: "The number of days remaining until the certificate expires",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorCertIsValid) {
|
||||||
|
this.monitorCertIsValid = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_cert_is_valid",
|
||||||
|
help: "Is the certificate still valid? (1 = Yes, 0 = No)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorResponseTime) {
|
||||||
|
this.monitorResponseTime = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_response_time",
|
||||||
|
help: "Monitor Response Time (ms)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.monitorStatus) {
|
||||||
|
this.monitorStatus = new PrometheusClient.Gauge({
|
||||||
|
name: "monitor_status",
|
||||||
|
help: "Monitor Status (1 = UP, 0 = DOWN, 2 = PENDING, 3 = MAINTENANCE)",
|
||||||
|
labelNames: this.monitorLabelNames
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to create a `Prometheus` instance and ensure metrics are initialized.
|
||||||
|
* @param {Monitor} monitor Monitor object to monitor
|
||||||
|
* @returns {Promise<Prometheus>} `Prometheus` instance
|
||||||
|
*/
|
||||||
|
static async createAndInitMetrics(monitor) {
|
||||||
|
await Prometheus.initMetrics();
|
||||||
|
let tags = await monitor.getTags();
|
||||||
|
return new Prometheus(monitor, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a prometheus metric instance.
|
||||||
|
*
|
||||||
|
* Note: Make sure to call `Prometheus.initMetrics()` once prior creating Prometheus instances.
|
||||||
|
* @param {Monitor} monitor Monitor object to monitor
|
||||||
|
* @param {Promise<LooseObject<any>[]>} tags Tags of the monitor
|
||||||
|
*/
|
||||||
|
constructor(monitor, tags) {
|
||||||
this.monitorLabelValues = {
|
this.monitorLabelValues = {
|
||||||
monitor_name: monitor.name,
|
monitor_name: monitor.name,
|
||||||
monitor_type: monitor.type,
|
monitor_type: monitor.type,
|
||||||
@@ -46,6 +113,12 @@ class Prometheus {
|
|||||||
monitor_hostname: monitor.hostname,
|
monitor_hostname: monitor.hostname,
|
||||||
monitor_port: monitor.port
|
monitor_port: monitor.port
|
||||||
};
|
};
|
||||||
|
Object.values(tags)
|
||||||
|
// only label names that were known at first metric creation.
|
||||||
|
.filter(tag => Prometheus.monitorLabelNames.includes(tag.name))
|
||||||
|
.forEach(tag => {
|
||||||
|
this.monitorLabelValues[tag.name] = tag.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +128,6 @@ class Prometheus {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
update(heartbeat, tlsInfo) {
|
update(heartbeat, tlsInfo) {
|
||||||
|
|
||||||
if (typeof tlsInfo !== "undefined") {
|
if (typeof tlsInfo !== "undefined") {
|
||||||
try {
|
try {
|
||||||
let isValid;
|
let isValid;
|
||||||
@@ -64,7 +136,7 @@ class Prometheus {
|
|||||||
} else {
|
} else {
|
||||||
isValid = 0;
|
isValid = 0;
|
||||||
}
|
}
|
||||||
monitorCertIsValid.set(this.monitorLabelValues, isValid);
|
Prometheus.monitorCertIsValid.set(this.monitorLabelValues, isValid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
log.error("prometheus", e);
|
log.error("prometheus", e);
|
||||||
@@ -72,7 +144,7 @@ class Prometheus {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (tlsInfo.certInfo != null) {
|
if (tlsInfo.certInfo != null) {
|
||||||
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
Prometheus.monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
@@ -82,7 +154,7 @@ class Prometheus {
|
|||||||
|
|
||||||
if (heartbeat) {
|
if (heartbeat) {
|
||||||
try {
|
try {
|
||||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
Prometheus.monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
log.error("prometheus", e);
|
log.error("prometheus", e);
|
||||||
@@ -90,10 +162,10 @@ class Prometheus {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof heartbeat.ping === "number") {
|
if (typeof heartbeat.ping === "number") {
|
||||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
Prometheus.monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||||
} else {
|
} else {
|
||||||
// Is it good?
|
// Is it good?
|
||||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
Prometheus.monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("prometheus", "Caught error");
|
log.error("prometheus", "Caught error");
|
||||||
@@ -108,10 +180,10 @@ class Prometheus {
|
|||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
try {
|
try {
|
||||||
monitorCertDaysRemaining.remove(this.monitorLabelValues);
|
Prometheus.monitorCertDaysRemaining?.remove(this.monitorLabelValues);
|
||||||
monitorCertIsValid.remove(this.monitorLabelValues);
|
Prometheus.monitorCertIsValid?.remove(this.monitorLabelValues);
|
||||||
monitorResponseTime.remove(this.monitorLabelValues);
|
Prometheus.monitorResponseTime?.remove(this.monitorLabelValues);
|
||||||
monitorStatus.remove(this.monitorLabelValues);
|
Prometheus.monitorStatus?.remove(this.monitorLabelValues);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ const { R } = require("redbean-node");
|
|||||||
const HttpProxyAgent = require("http-proxy-agent");
|
const HttpProxyAgent = require("http-proxy-agent");
|
||||||
const HttpsProxyAgent = require("https-proxy-agent");
|
const HttpsProxyAgent = require("https-proxy-agent");
|
||||||
const SocksProxyAgent = require("socks-proxy-agent");
|
const SocksProxyAgent = require("socks-proxy-agent");
|
||||||
const { debug } = require("../src/util");
|
const { log } = require("../src/util");
|
||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
const { CookieJar } = require("tough-cookie");
|
const { CookieJar } = require("tough-cookie");
|
||||||
const { createCookieAgent } = require("http-cookie-agent/http");
|
const { createCookieAgent } = require("http-cookie-agent/http");
|
||||||
@@ -110,9 +110,9 @@ class Proxy {
|
|||||||
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
|
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
|
log.debug("update-proxy", `Proxy Options: ${JSON.stringify(proxyOptions)}`);
|
||||||
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
|
log.debug("update-proxy", `HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
|
||||||
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
|
log.debug("update-proxy", `HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
|
||||||
|
|
||||||
switch (proxy.protocol) {
|
switch (proxy.protocol) {
|
||||||
case "http":
|
case "http":
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
let express = require("express");
|
let express = require("express");
|
||||||
const {
|
const {
|
||||||
setting,
|
|
||||||
allowDevAllOrigin,
|
allowDevAllOrigin,
|
||||||
allowAllOrigin,
|
allowAllOrigin,
|
||||||
percentageToColor,
|
percentageToColor,
|
||||||
@@ -18,6 +17,7 @@ const { makeBadge } = require("badge-maker");
|
|||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const { UptimeCalculator } = require("../uptime-calculator");
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ router.get("/api/entry-page", async (request, response) => {
|
|||||||
|
|
||||||
let result = { };
|
let result = { };
|
||||||
let hostname = request.hostname;
|
let hostname = request.hostname;
|
||||||
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
|
if ((await Settings.get("trustProxy")) && request.headers["x-forwarded-host"]) {
|
||||||
hostname = request.headers["x-forwarded-host"];
|
hostname = request.headers["x-forwarded-host"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -90,8 +90,7 @@ const Monitor = require("./model/monitor");
|
|||||||
const User = require("./model/user");
|
const User = require("./model/user");
|
||||||
|
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
const { initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin } = require("./util-server");
|
||||||
} = require("./util-server");
|
|
||||||
|
|
||||||
log.debug("server", "Importing Notification");
|
log.debug("server", "Importing Notification");
|
||||||
const { Notification } = require("./notification");
|
const { Notification } = require("./notification");
|
||||||
@@ -201,7 +200,7 @@ let needSetup = false;
|
|||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (request, response) => {
|
app.get("/", async (request, response) => {
|
||||||
let hostname = request.hostname;
|
let hostname = request.hostname;
|
||||||
if (await setting("trustProxy")) {
|
if (await Settings.get("trustProxy")) {
|
||||||
const proxy = request.headers["x-forwarded-host"];
|
const proxy = request.headers["x-forwarded-host"];
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
hostname = proxy;
|
hostname = proxy;
|
||||||
@@ -281,7 +280,7 @@ let needSetup = false;
|
|||||||
// Robots.txt
|
// Robots.txt
|
||||||
app.get("/robots.txt", async (_request, response) => {
|
app.get("/robots.txt", async (_request, response) => {
|
||||||
let txt = "User-agent: *\nDisallow:";
|
let txt = "User-agent: *\nDisallow:";
|
||||||
if (!await setting("searchEngineIndex")) {
|
if (!await Settings.get("searchEngineIndex")) {
|
||||||
txt += " /";
|
txt += " /";
|
||||||
}
|
}
|
||||||
response.setHeader("Content-Type", "text/plain");
|
response.setHeader("Content-Type", "text/plain");
|
||||||
@@ -718,8 +717,6 @@ let needSetup = false;
|
|||||||
|
|
||||||
monitor.conditions = JSON.stringify(monitor.conditions);
|
monitor.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
|
||||||
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
|
|
||||||
|
|
||||||
bean.import(monitor);
|
bean.import(monitor);
|
||||||
bean.user_id = socket.userID;
|
bean.user_id = socket.userID;
|
||||||
|
|
||||||
@@ -729,7 +726,7 @@ let needSetup = false;
|
|||||||
|
|
||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
await server.sendUpdateMonitorIntoList(socket, bean.id);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
if (monitor.active !== false) {
|
if (monitor.active !== false) {
|
||||||
await startMonitor(socket.userID, bean.id);
|
await startMonitor(socket.userID, bean.id);
|
||||||
@@ -870,9 +867,6 @@ let needSetup = false;
|
|||||||
bean.snmpOid = monitor.snmpOid;
|
bean.snmpOid = monitor.snmpOid;
|
||||||
bean.jsonPathOperator = monitor.jsonPathOperator;
|
bean.jsonPathOperator = monitor.jsonPathOperator;
|
||||||
bean.timeout = monitor.timeout;
|
bean.timeout = monitor.timeout;
|
||||||
bean.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
|
|
||||||
bean.rabbitmqUsername = monitor.rabbitmqUsername;
|
|
||||||
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
|
||||||
bean.conditions = JSON.stringify(monitor.conditions);
|
bean.conditions = JSON.stringify(monitor.conditions);
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
@@ -885,11 +879,11 @@ let needSetup = false;
|
|||||||
|
|
||||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||||
|
|
||||||
if (await Monitor.isActive(bean.id, bean.active)) {
|
if (await bean.isActive()) {
|
||||||
await restartMonitor(socket.userID, bean.id);
|
await restartMonitor(socket.userID, bean.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await server.sendUpdateMonitorIntoList(socket, bean.id);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -929,17 +923,14 @@ let needSetup = false;
|
|||||||
|
|
||||||
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
|
||||||
monitorID,
|
monitorID,
|
||||||
socket.userID,
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
const monitorData = [{ id: monitor.id,
|
|
||||||
active: monitor.active
|
|
||||||
}];
|
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
monitor: monitor.toJSON(preloadData),
|
monitor: await bean.toJSON(),
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -990,7 +981,7 @@ let needSetup = false;
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await startMonitor(socket.userID, monitorID);
|
await startMonitor(socket.userID, monitorID);
|
||||||
await server.sendUpdateMonitorIntoList(socket, monitorID);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -1010,7 +1001,7 @@ let needSetup = false;
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await pauseMonitor(socket.userID, monitorID);
|
await pauseMonitor(socket.userID, monitorID);
|
||||||
await server.sendUpdateMonitorIntoList(socket, monitorID);
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -1056,7 +1047,8 @@ let needSetup = false;
|
|||||||
msg: "successDeleted",
|
msg: "successDeleted",
|
||||||
msgi18n: true,
|
msgi18n: true,
|
||||||
});
|
});
|
||||||
await server.sendDeleteMonitorFromList(socket, monitorID);
|
|
||||||
|
await server.sendMonitorList(socket);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
@@ -1332,7 +1324,7 @@ let needSetup = false;
|
|||||||
socket.on("getSettings", async (callback) => {
|
socket.on("getSettings", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const data = await getSettings("general");
|
const data = await Settings.getSettings("general");
|
||||||
|
|
||||||
if (!data.serverTimezone) {
|
if (!data.serverTimezone) {
|
||||||
data.serverTimezone = await server.getTimezone();
|
data.serverTimezone = await server.getTimezone();
|
||||||
@@ -1360,7 +1352,7 @@ let needSetup = false;
|
|||||||
// Disabled Auth + Want to Enable Auth => No Check
|
// Disabled Auth + Want to Enable Auth => No Check
|
||||||
// Enabled Auth + Want to Disable Auth => Check!!
|
// Enabled Auth + Want to Disable Auth => Check!!
|
||||||
// Enabled Auth + Want to Enable Auth => No Check
|
// Enabled Auth + Want to Enable Auth => No Check
|
||||||
const currentDisabledAuth = await setting("disableAuth");
|
const currentDisabledAuth = await Settings.get("disableAuth");
|
||||||
if (!currentDisabledAuth && data.disableAuth) {
|
if (!currentDisabledAuth && data.disableAuth) {
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
}
|
}
|
||||||
@@ -1374,7 +1366,7 @@ let needSetup = false;
|
|||||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||||
const previousNSCDStatus = await Settings.get("nscd");
|
const previousNSCDStatus = await Settings.get("nscd");
|
||||||
|
|
||||||
await setSettings("general", data);
|
await Settings.setSettings("general", data);
|
||||||
server.entryPage = data.entryPage;
|
server.entryPage = data.entryPage;
|
||||||
|
|
||||||
// Also need to apply timezone globally
|
// Also need to apply timezone globally
|
||||||
@@ -1470,7 +1462,7 @@ let needSetup = false;
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("server", e);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -1583,7 +1575,7 @@ let needSetup = false;
|
|||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
log.debug("auth", "check auto login");
|
log.debug("auth", "check auto login");
|
||||||
if (await setting("disableAuth")) {
|
if (await Settings.get("disableAuth")) {
|
||||||
log.info("auth", "Disabled Auth: auto login to admin");
|
log.info("auth", "Disabled Auth: auto login to admin");
|
||||||
await afterLogin(socket, await R.findOne("user"));
|
await afterLogin(socket, await R.findOne("user"));
|
||||||
socket.emit("autoLogin");
|
socket.emit("autoLogin");
|
||||||
@@ -1604,20 +1596,18 @@ let needSetup = false;
|
|||||||
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
server.httpServer.listen(port, hostname, async () => {
|
server.httpServer.listen(port, hostname, () => {
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
log.info("server", `Listening on ${hostname}:${port}`);
|
log.info("server", `Listening on ${hostname}:${port}`);
|
||||||
} else {
|
} else {
|
||||||
log.info("server", `Listening on ${port}`);
|
log.info("server", `Listening on ${port}`);
|
||||||
}
|
}
|
||||||
await startMonitors();
|
startMonitors();
|
||||||
|
|
||||||
// Put this here. Start background jobs after the db and server is ready to prevent clear up during db migration.
|
|
||||||
await initBackgroundJobs();
|
|
||||||
|
|
||||||
checkVersion.startInterval();
|
checkVersion.startInterval();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await initBackgroundJobs();
|
||||||
|
|
||||||
// Start cloudflared at the end if configured
|
// Start cloudflared at the end if configured
|
||||||
await cloudflaredAutoStart(cloudflaredToken);
|
await cloudflaredAutoStart(cloudflaredToken);
|
||||||
|
|
||||||
@@ -1688,13 +1678,13 @@ async function afterLogin(socket, user) {
|
|||||||
|
|
||||||
await StatusPage.sendStatusPageList(io, socket);
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
|
|
||||||
const monitorPromises = [];
|
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
monitorPromises.push(sendHeartbeatList(socket, monitorID));
|
await sendHeartbeatList(socket, monitorID);
|
||||||
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(monitorPromises);
|
for (let monitorID in monitorList) {
|
||||||
|
await Monitor.sendStats(io, monitorID, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Set server timezone from client browser if not set
|
// Set server timezone from client browser if not set
|
||||||
// It should be run once only
|
// It should be run once only
|
||||||
@@ -1716,7 +1706,7 @@ async function initDatabase(testMode = false) {
|
|||||||
log.info("server", "Connected to the database");
|
log.info("server", "Connected to the database");
|
||||||
|
|
||||||
// Patch the database
|
// Patch the database
|
||||||
await Database.patch(port, hostname);
|
await Database.patch();
|
||||||
|
|
||||||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
||||||
"jwtSecret",
|
"jwtSecret",
|
||||||
@@ -1811,11 +1801,7 @@ async function startMonitors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let monitor of list) {
|
for (let monitor of list) {
|
||||||
try {
|
await monitor.start(io);
|
||||||
await monitor.start(io);
|
|
||||||
} catch (e) {
|
|
||||||
log.error("monitor", e);
|
|
||||||
}
|
|
||||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||||
await sleep(getRandomInt(300, 1000));
|
await sleep(getRandomInt(300, 1000));
|
||||||
}
|
}
|
||||||
|
@@ -60,7 +60,7 @@ module.exports.apiKeySocketHandler = (socket) => {
|
|||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("apikeys", e);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
|
const { checkLogin, doubleCheckPassword } = require("../util-server");
|
||||||
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { log } = require("../../src/util");
|
const { log } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
const io = UptimeKumaServer.getInstance().io;
|
const io = UptimeKumaServer.getInstance().io;
|
||||||
|
|
||||||
const prefix = "cloudflared_";
|
const prefix = "cloudflared_";
|
||||||
@@ -40,7 +41,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
socket.join("cloudflared");
|
socket.join("cloudflared");
|
||||||
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
|
||||||
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
|
||||||
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
|
io.to(socket.userID).emit(prefix + "token", await Settings.get("cloudflaredTunnelToken"));
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
if (token && typeof token === "string") {
|
if (token && typeof token === "string") {
|
||||||
await setSetting("cloudflaredTunnelToken", token);
|
await Settings.set("cloudflaredTunnelToken", token);
|
||||||
cloudflared.token = token;
|
cloudflared.token = token;
|
||||||
} else {
|
} else {
|
||||||
cloudflared.token = null;
|
cloudflared.token = null;
|
||||||
@@ -67,7 +68,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
socket.on(prefix + "stop", async (currentPassword, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
const disabledAuth = await setting("disableAuth");
|
const disabledAuth = await Settings.get("disableAuth");
|
||||||
if (!disabledAuth) {
|
if (!disabledAuth) {
|
||||||
await doubleCheckPassword(socket, currentPassword);
|
await doubleCheckPassword(socket, currentPassword);
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
socket.on(prefix + "removeToken", async () => {
|
socket.on(prefix + "removeToken", async () => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
await setSetting("cloudflaredTunnelToken", "");
|
await Settings.set("cloudflaredTunnelToken", "");
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,15 +97,15 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
|||||||
*/
|
*/
|
||||||
module.exports.autoStart = async (token) => {
|
module.exports.autoStart = async (token) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = await setting("cloudflaredTunnelToken");
|
token = await Settings.get("cloudflaredTunnelToken");
|
||||||
} else {
|
} else {
|
||||||
// Override the current token via args or env var
|
// Override the current token via args or env var
|
||||||
await setSetting("cloudflaredTunnelToken", token);
|
await Settings.set("cloudflaredTunnelToken", token);
|
||||||
console.log("Use cloudflared token from args or env var");
|
log.info("cloudflare", "Use cloudflared token from args or env var");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log("Start cloudflared");
|
log.info("cloudflare", "Start cloudflared");
|
||||||
cloudflared.token = token;
|
cloudflared.token = token;
|
||||||
cloudflared.start();
|
cloudflared.start();
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("maintenance", e);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
@@ -177,7 +177,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
|||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("maintenance", e);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
@@ -201,7 +201,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("maintenance", e);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
@@ -225,7 +225,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
log.error("maintenance", e);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { checkLogin, setSetting } = require("../util-server");
|
const { checkLogin } = require("../util-server");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { log } = require("../../src/util");
|
const { log } = require("../../src/util");
|
||||||
const ImageDataURI = require("../image-data-uri");
|
const ImageDataURI = require("../image-data-uri");
|
||||||
@@ -7,6 +7,7 @@ const Database = require("../database");
|
|||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket handlers for status page
|
* Socket handlers for status page
|
||||||
@@ -233,7 +234,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
// Also change entry page to new slug if it is the default one, and slug is changed.
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||||
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||||
server.entryPage = "statusPage-" + statusPage.slug;
|
server.entryPage = "statusPage-" + statusPage.slug;
|
||||||
await setSetting("entryPage", server.entryPage, "general");
|
await Settings.set("entryPage", server.entryPage, "general");
|
||||||
}
|
}
|
||||||
|
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
@@ -291,7 +292,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
log.error("socket", error);
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: error.message,
|
msg: error.message,
|
||||||
@@ -313,7 +314,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
|||||||
// Reset entry page if it is the default one.
|
// Reset entry page if it is the default one.
|
||||||
if (server.entryPage === "statusPage-" + slug) {
|
if (server.entryPage === "statusPage-" + slug) {
|
||||||
server.entryPage = "dashboard";
|
server.entryPage = "dashboard";
|
||||||
await setSetting("entryPage", server.entryPage, "general");
|
await Settings.set("entryPage", server.entryPage, "general");
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||||
|
@@ -12,6 +12,7 @@ class UptimeCalculator {
|
|||||||
* @private
|
* @private
|
||||||
* @type {{string:UptimeCalculator}}
|
* @type {{string:UptimeCalculator}}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static list = {};
|
static list = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,15 +55,6 @@ class UptimeCalculator {
|
|||||||
lastHourlyStatBean = null;
|
lastHourlyStatBean = null;
|
||||||
lastMinutelyStatBean = null;
|
lastMinutelyStatBean = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* For migration purposes.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
migrationMode = false;
|
|
||||||
|
|
||||||
statMinutelyKeepHour = 24;
|
|
||||||
statHourlyKeepDay = 30;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the uptime calculator for a monitor
|
* Get the uptime calculator for a monitor
|
||||||
* Initializes and returns the monitor if it does not exist
|
* Initializes and returns the monitor if it does not exist
|
||||||
@@ -197,19 +189,16 @@ class UptimeCalculator {
|
|||||||
/**
|
/**
|
||||||
* @param {number} status status
|
* @param {number} status status
|
||||||
* @param {number} ping Ping
|
* @param {number} ping Ping
|
||||||
* @param {dayjs.Dayjs} date Date (Only for migration)
|
|
||||||
* @returns {dayjs.Dayjs} date
|
* @returns {dayjs.Dayjs} date
|
||||||
* @throws {Error} Invalid status
|
* @throws {Error} Invalid status
|
||||||
*/
|
*/
|
||||||
async update(status, ping = 0, date) {
|
async update(status, ping = 0) {
|
||||||
if (!date) {
|
let date = this.getCurrentDate();
|
||||||
date = this.getCurrentDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
let flatStatus = this.flatStatus(status);
|
let flatStatus = this.flatStatus(status);
|
||||||
|
|
||||||
if (flatStatus === DOWN && ping > 0) {
|
if (flatStatus === DOWN && ping > 0) {
|
||||||
log.debug("uptime-calc", "The ping is not effective when the status is DOWN");
|
log.warn("uptime-calc", "The ping is not effective when the status is DOWN");
|
||||||
}
|
}
|
||||||
|
|
||||||
let divisionKey = this.getMinutelyKey(date);
|
let divisionKey = this.getMinutelyKey(date);
|
||||||
@@ -308,61 +297,47 @@ class UptimeCalculator {
|
|||||||
}
|
}
|
||||||
await R.store(dailyStatBean);
|
await R.store(dailyStatBean);
|
||||||
|
|
||||||
let currentDate = this.getCurrentDate();
|
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
|
||||||
|
hourlyStatBean.up = hourlyData.up;
|
||||||
// For migration mode, we don't need to store old hourly and minutely data, but we need 30-day's hourly data
|
hourlyStatBean.down = hourlyData.down;
|
||||||
// Run anyway for non-migration mode
|
hourlyStatBean.ping = hourlyData.avgPing;
|
||||||
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statHourlyKeepDay, "day"))) {
|
hourlyStatBean.pingMin = hourlyData.minPing;
|
||||||
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
|
hourlyStatBean.pingMax = hourlyData.maxPing;
|
||||||
hourlyStatBean.up = hourlyData.up;
|
{
|
||||||
hourlyStatBean.down = hourlyData.down;
|
// eslint-disable-next-line no-unused-vars
|
||||||
hourlyStatBean.ping = hourlyData.avgPing;
|
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
|
||||||
hourlyStatBean.pingMin = hourlyData.minPing;
|
if (Object.keys(extras).length > 0) {
|
||||||
hourlyStatBean.pingMax = hourlyData.maxPing;
|
hourlyStatBean.extras = JSON.stringify(extras);
|
||||||
{
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
|
|
||||||
if (Object.keys(extras).length > 0) {
|
|
||||||
hourlyStatBean.extras = JSON.stringify(extras);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await R.store(hourlyStatBean);
|
|
||||||
}
|
}
|
||||||
|
await R.store(hourlyStatBean);
|
||||||
|
|
||||||
// For migration mode, we don't need to store old hourly and minutely data, but we need 24-hour's minutely data
|
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
||||||
// Run anyway for non-migration mode
|
minutelyStatBean.up = minutelyData.up;
|
||||||
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statMinutelyKeepHour, "hour"))) {
|
minutelyStatBean.down = minutelyData.down;
|
||||||
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
minutelyStatBean.ping = minutelyData.avgPing;
|
||||||
minutelyStatBean.up = minutelyData.up;
|
minutelyStatBean.pingMin = minutelyData.minPing;
|
||||||
minutelyStatBean.down = minutelyData.down;
|
minutelyStatBean.pingMax = minutelyData.maxPing;
|
||||||
minutelyStatBean.ping = minutelyData.avgPing;
|
{
|
||||||
minutelyStatBean.pingMin = minutelyData.minPing;
|
// eslint-disable-next-line no-unused-vars
|
||||||
minutelyStatBean.pingMax = minutelyData.maxPing;
|
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
|
||||||
{
|
if (Object.keys(extras).length > 0) {
|
||||||
// eslint-disable-next-line no-unused-vars
|
minutelyStatBean.extras = JSON.stringify(extras);
|
||||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
|
|
||||||
if (Object.keys(extras).length > 0) {
|
|
||||||
minutelyStatBean.extras = JSON.stringify(extras);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await R.store(minutelyStatBean);
|
|
||||||
}
|
}
|
||||||
|
await R.store(minutelyStatBean);
|
||||||
|
|
||||||
// No need to remove old data in migration mode
|
// Remove the old data
|
||||||
if (!this.migrationMode) {
|
log.debug("uptime-calc", "Remove old data");
|
||||||
// Remove the old data
|
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
|
||||||
// TODO: Improvement: Convert it to a job?
|
this.monitorID,
|
||||||
log.debug("uptime-calc", "Remove old data");
|
this.getMinutelyKey(date.subtract(24, "hour")),
|
||||||
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
|
]);
|
||||||
this.monitorID,
|
|
||||||
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
|
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
|
||||||
this.monitorID,
|
this.monitorID,
|
||||||
this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day")),
|
this.getHourlyKey(date.subtract(30, "day")),
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
@@ -837,14 +812,6 @@ class UptimeCalculator {
|
|||||||
return dayjs.utc();
|
return dayjs.utc();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For migration purposes.
|
|
||||||
* @param {boolean} value Migration mode on/off
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
setMigrationMode(value) {
|
|
||||||
this.migrationMode = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UptimeDataResult {
|
class UptimeDataResult {
|
||||||
|
@@ -115,7 +115,6 @@ class UptimeKumaServer {
|
|||||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
|
|
||||||
|
|
||||||
// Allow all CORS origins (polling) in development
|
// Allow all CORS origins (polling) in development
|
||||||
let cors = undefined;
|
let cors = undefined;
|
||||||
@@ -206,56 +205,24 @@ class UptimeKumaServer {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update Monitor into list
|
|
||||||
* @param {Socket} socket Socket to send list on
|
|
||||||
* @param {number} monitorID update or deleted monitor id
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async sendUpdateMonitorIntoList(socket, monitorID) {
|
|
||||||
let list = await this.getMonitorJSONList(socket.userID, monitorID);
|
|
||||||
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete Monitor from list
|
|
||||||
* @param {Socket} socket Socket to send list on
|
|
||||||
* @param {number} monitorID update or deleted monitor id
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async sendDeleteMonitorFromList(socket, monitorID) {
|
|
||||||
this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of monitors for the given user.
|
* Get a list of monitors for the given user.
|
||||||
* @param {string} userID - The ID of the user to get monitors for.
|
* @param {string} userID - The ID of the user to get monitors for.
|
||||||
* @param {number} monitorID - The ID of monitor for.
|
|
||||||
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
async getMonitorJSONList(userID, monitorID = null) {
|
async getMonitorJSONList(userID) {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
let query = " user_id = ? ";
|
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
|
||||||
let queryParams = [ userID ];
|
userID,
|
||||||
|
]);
|
||||||
|
|
||||||
if (monitorID) {
|
for (let monitor of monitorList) {
|
||||||
query += "AND id = ? ";
|
result[monitor.id] = await monitor.toJSON();
|
||||||
queryParams.push(monitorID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
|
|
||||||
|
|
||||||
const monitorData = monitorList.map(monitor => ({
|
|
||||||
id: monitor.id,
|
|
||||||
active: monitor.active,
|
|
||||||
name: monitor.name,
|
|
||||||
}));
|
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData));
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,5 +520,3 @@ const { DnsMonitorType } = require("./monitor-types/dns");
|
|||||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||||
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
const { SNMPMonitorType } = require("./monitor-types/snmp");
|
||||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||||
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
|
|
||||||
const Monitor = require("./model/monitor");
|
|
||||||
|
@@ -12,7 +12,6 @@ const { Client } = require("pg");
|
|||||||
const postgresConParse = require("pg-connection-string").parse;
|
const postgresConParse = require("pg-connection-string").parse;
|
||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
|
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
|
||||||
const { Settings } = require("./settings");
|
|
||||||
const grpc = require("@grpc/grpc-js");
|
const grpc = require("@grpc/grpc-js");
|
||||||
const protojs = require("protobufjs");
|
const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
@@ -521,46 +520,6 @@ exports.redisPingAsync = function (dsn, rejectUnauthorized) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve value of setting based on key
|
|
||||||
* @param {string} key Key of setting to retrieve
|
|
||||||
* @returns {Promise<any>} Value
|
|
||||||
* @deprecated Use await Settings.get(key)
|
|
||||||
*/
|
|
||||||
exports.setting = async function (key) {
|
|
||||||
return await Settings.get(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the specified setting to specified value
|
|
||||||
* @param {string} key Key of setting to set
|
|
||||||
* @param {any} value Value to set to
|
|
||||||
* @param {?string} type Type of setting
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
exports.setSetting = async function (key, value, type = null) {
|
|
||||||
await Settings.set(key, value, type);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get settings based on type
|
|
||||||
* @param {string} type The type of setting
|
|
||||||
* @returns {Promise<Bean>} Settings of requested type
|
|
||||||
*/
|
|
||||||
exports.getSettings = async function (type) {
|
|
||||||
return await Settings.getSettings(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set settings based on type
|
|
||||||
* @param {string} type Type of settings to set
|
|
||||||
* @param {object} data Values of settings
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
exports.setSettings = async function (type, data) {
|
|
||||||
await Settings.setSettings(type, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
|
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts
|
||||||
|
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
const ColumnCompilerMySQL = require("knex/lib/dialects/mysql/schema/mysql-columncompiler");
|
|
||||||
const { formatDefault } = require("knex/lib/formatter/formatterUtils");
|
|
||||||
const { log } = require("../../../../../../../src/util");
|
|
||||||
|
|
||||||
class KumaColumnCompiler extends ColumnCompilerMySQL {
|
|
||||||
/**
|
|
||||||
* Override defaultTo method to handle default value for TEXT fields
|
|
||||||
* @param {any} value Value
|
|
||||||
* @returns {string|void} Default value (Don't understand why it can return void or string, but it's the original code, lol)
|
|
||||||
*/
|
|
||||||
defaultTo(value) {
|
|
||||||
if (this.type === "text" && typeof value === "string") {
|
|
||||||
log.debug("defaultTo", `${this.args[0]}: ${this.type} ${value} ${typeof value}`);
|
|
||||||
// MySQL 8.0 is required and only if the value is written as an expression: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
|
|
||||||
// MariaDB 10.2 is required: https://mariadb.com/kb/en/text/
|
|
||||||
return `default (${formatDefault(value, this.type, this.client)})`;
|
|
||||||
}
|
|
||||||
return super.defaultTo.apply(this, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = KumaColumnCompiler;
|
|
@@ -1,84 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const http = require("node:http");
|
|
||||||
const { log } = require("../../src/util");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SimpleMigrationServer
|
|
||||||
* For displaying the migration status of the server
|
|
||||||
* Also, it is used to let Docker healthcheck know the status of the server, as the main server is not started yet, healthcheck will think the server is down incorrectly.
|
|
||||||
*/
|
|
||||||
class SimpleMigrationServer {
|
|
||||||
/**
|
|
||||||
* Express app instance
|
|
||||||
* @type {?Express}
|
|
||||||
*/
|
|
||||||
app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server instance
|
|
||||||
* @type {?Server}
|
|
||||||
*/
|
|
||||||
server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response object
|
|
||||||
* @type {?Response}
|
|
||||||
*/
|
|
||||||
response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the server
|
|
||||||
* @param {number} port Port
|
|
||||||
* @param {string} hostname Hostname
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
start(port, hostname) {
|
|
||||||
this.app = express();
|
|
||||||
this.server = http.createServer(this.app);
|
|
||||||
|
|
||||||
this.app.get("/", (req, res) => {
|
|
||||||
res.set("Content-Type", "text/plain");
|
|
||||||
res.write("Migration is in progress, listening message...\n");
|
|
||||||
if (this.response) {
|
|
||||||
this.response.write("Disconnected\n");
|
|
||||||
this.response.end();
|
|
||||||
}
|
|
||||||
this.response = res;
|
|
||||||
// never ending response
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.server.listen(port, hostname, () => {
|
|
||||||
if (hostname) {
|
|
||||||
log.info("migration", `Migration server is running on http://${hostname}:${port}`);
|
|
||||||
} else {
|
|
||||||
log.info("migration", `Migration server is running on http://localhost:${port}`);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the message
|
|
||||||
* @param {string} msg Message to update
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
update(msg) {
|
|
||||||
this.response?.write(msg + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the server
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async stop() {
|
|
||||||
this.response?.write("Finished, please refresh this page.\n");
|
|
||||||
this.response?.end();
|
|
||||||
await this.server?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
SimpleMigrationServer,
|
|
||||||
};
|
|
@@ -4,17 +4,11 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="beat-hover-area"
|
class="beat"
|
||||||
:class="{ 'empty': (beat === 0) }"
|
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
||||||
:style="beatHoverAreaStyle"
|
:style="beatStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
class="beat"
|
|
||||||
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
|
||||||
:style="beatStyle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
|
||||||
@@ -53,7 +47,7 @@ export default {
|
|||||||
beatWidth: 10,
|
beatWidth: 10,
|
||||||
beatHeight: 30,
|
beatHeight: 30,
|
||||||
hoverScale: 1.5,
|
hoverScale: 1.5,
|
||||||
beatHoverAreaPadding: 4,
|
beatMargin: 4,
|
||||||
move: false,
|
move: false,
|
||||||
maxBeat: -1,
|
maxBeat: -1,
|
||||||
};
|
};
|
||||||
@@ -129,7 +123,7 @@ export default {
|
|||||||
|
|
||||||
barStyle() {
|
barStyle() {
|
||||||
if (this.move && this.shortBeatList.length > this.maxBeat) {
|
if (this.move && this.shortBeatList.length > this.maxBeat) {
|
||||||
let width = -(this.beatWidth + this.beatHoverAreaPadding * 2);
|
let width = -(this.beatWidth + this.beatMargin * 2);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transition: "all ease-in-out 0.25s",
|
transition: "all ease-in-out 0.25s",
|
||||||
@@ -143,17 +137,12 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beatHoverAreaStyle() {
|
|
||||||
return {
|
|
||||||
padding: this.beatHoverAreaPadding + "px",
|
|
||||||
"--hover-scale": this.hoverScale,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
beatStyle() {
|
beatStyle() {
|
||||||
return {
|
return {
|
||||||
width: this.beatWidth + "px",
|
width: this.beatWidth + "px",
|
||||||
height: this.beatHeight + "px",
|
height: this.beatHeight + "px",
|
||||||
|
margin: this.beatMargin + "px",
|
||||||
|
"--hover-scale": this.hoverScale,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -163,7 +152,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
timeStyle() {
|
timeStyle() {
|
||||||
return {
|
return {
|
||||||
"margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px",
|
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -230,20 +219,20 @@ export default {
|
|||||||
if (this.size !== "big") {
|
if (this.size !== "big") {
|
||||||
this.beatWidth = 5;
|
this.beatWidth = 5;
|
||||||
this.beatHeight = 16;
|
this.beatHeight = 16;
|
||||||
this.beatHoverAreaPadding = 2;
|
this.beatMargin = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suddenly, have an idea how to handle it universally.
|
// Suddenly, have an idea how to handle it universally.
|
||||||
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
|
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
|
||||||
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
const actualWidth = this.beatWidth * window.devicePixelRatio;
|
||||||
const actualHoverAreaPadding = this.beatHoverAreaPadding * window.devicePixelRatio;
|
const actualMargin = this.beatMargin * window.devicePixelRatio;
|
||||||
|
|
||||||
if (!Number.isInteger(actualWidth)) {
|
if (!Number.isInteger(actualWidth)) {
|
||||||
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(actualHoverAreaPadding)) {
|
if (!Number.isInteger(actualMargin)) {
|
||||||
this.beatHoverAreaPadding = Math.round(actualHoverAreaPadding) / window.devicePixelRatio;
|
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", this.resize);
|
window.addEventListener("resize", this.resize);
|
||||||
@@ -256,7 +245,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -284,41 +273,32 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hp-bar-big {
|
.hp-bar-big {
|
||||||
.beat-hover-area {
|
.beat {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
background-color: $primary;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background-color: aliceblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.maintenance {
|
||||||
|
background-color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.empty):hover {
|
&:not(.empty):hover {
|
||||||
transition: all ease-in-out 0.15s;
|
transition: all ease-in-out 0.15s;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transform: scale(var(--hover-scale));
|
transform: scale(var(--hover-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat {
|
|
||||||
background-color: $primary;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
|
|
||||||
/*
|
|
||||||
pointer-events needs to be changed because
|
|
||||||
tooltip momentarily disappears when crossing between .beat-hover-area and .beat
|
|
||||||
*/
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&.empty {
|
|
||||||
background-color: aliceblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.down {
|
|
||||||
background-color: $danger;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pending {
|
|
||||||
background-color: $warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.maintenance {
|
|
||||||
background-color: $maintenance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -118,7 +118,6 @@ export default {
|
|||||||
"clicksendsms": "ClickSend SMS",
|
"clicksendsms": "ClickSend SMS",
|
||||||
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
|
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
|
||||||
"discord": "Discord",
|
"discord": "Discord",
|
||||||
"Elks": "46elks",
|
|
||||||
"GoogleChat": "Google Chat (Google Workspace)",
|
"GoogleChat": "Google Chat (Google Workspace)",
|
||||||
"gorush": "Gorush",
|
"gorush": "Gorush",
|
||||||
"gotify": "Gotify",
|
"gotify": "Gotify",
|
||||||
@@ -165,7 +164,6 @@ export default {
|
|||||||
"whapi": "WhatsApp (Whapi)",
|
"whapi": "WhatsApp (Whapi)",
|
||||||
"gtxmessaging": "GtxMessaging",
|
"gtxmessaging": "GtxMessaging",
|
||||||
"Cellsynt": "Cellsynt",
|
"Cellsynt": "Cellsynt",
|
||||||
"SendGrid": "SendGrid"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
<template #item="monitor">
|
<template #item="monitor">
|
||||||
<div class="item" data-testid="monitor">
|
<div class="item" data-testid="monitor">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9 col-md-8 small-padding">
|
<div class="col-6 col-md-4 small-padding">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
|
||||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="ElksUsername" class="form-label">{{ $t("Username") }}</label>
|
|
||||||
<input id="ElksUsername" v-model="$parent.notification.elksUsername" type="text" class="form-control" required>
|
|
||||||
<label for="ElksPassword" class="form-label">{{ $t("Password") }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">
|
|
||||||
<HiddenInput id="ElksPassword" v-model="$parent.notification.elksAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
|
|
||||||
<i18n-t tag="p" keypath="Can be found on:">
|
|
||||||
<a href="https://46elks.com/account" target="_blank">https://46elks.com/account</a>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="Elks-from-number" class="form-label">{{ $t("From") }}</label>
|
|
||||||
<input id="Elks-from-number" v-model="$parent.notification.elksFromNumber" type="text" class="form-control" required>
|
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.") }}
|
|
||||||
<i18n-t tag="p" keypath="More info on:">
|
|
||||||
<a href="https://46elks.se/kb/text-sender-id" target="_blank">https://46elks.se/kb/text-sender-id</a>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="Elks-to-number" class="form-label">{{ $t("To Number") }}</label>
|
|
||||||
<input id="Elks-to-number" v-model="$parent.notification.elksToNumber" type="text" class="form-control" required>
|
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("The phone number of the recipient in E.164 format.") }}
|
|
||||||
<i18n-t tag="p" keypath="More info on:">
|
|
||||||
<a href="https://46elks.se/kb/e164" target="_blank">https://46elks.se/kb/e164</a>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
|
||||||
<a href="https://46elks.com/docs/send-sms" target="_blank">https://46elks.com/docs/send-sms</a>
|
|
||||||
</i18n-t>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
HiddenInput,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-api-key" class="form-label">{{ $t("SendGrid API Key") }}</label>
|
|
||||||
<HiddenInput id="push-api-key" v-model="$parent.notification.sendgridApiKey" :required="true" autocomplete="new-password"></HiddenInput>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-from-email" class="form-label">{{ $t("From Email") }}</label>
|
|
||||||
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="email" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-to-email" class="form-label">{{ $t("To Email") }}</label>
|
|
||||||
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="email" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-cc-email" class="form-label">{{ $t("smtpCC") }}</label>
|
|
||||||
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="email" class="form-control">
|
|
||||||
<div class="form-text">{{ $t("Separate multiple email addresses with commas") }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-bcc-email" class="form-label">{{ $t("smtpBCC") }}</label>
|
|
||||||
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="email" class="form-control">
|
|
||||||
<small class="form-text text-muted">{{ $t("Separate multiple email addresses with commas") }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="sendgrid-subject" class="form-label">{{ $t("Subject:") }}</label>
|
|
||||||
<input id="sendgrid-subject" v-model="$parent.notification.sendgridSubject" type="text" class="form-control">
|
|
||||||
<small class="form-text text-muted">{{ $t("leave blank for default subject") }}</small>
|
|
||||||
</div>
|
|
||||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
|
||||||
<a href="https://docs.sendgrid.com/api-reference/mail-send/mail-send" target="_blank">https://docs.sendgrid.com/api-reference/mail-send/mail-send</a>
|
|
||||||
</i18n-t>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import HiddenInput from "../HiddenInput.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
HiddenInput,
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (typeof this.$parent.notification.sendgridSubject === "undefined") {
|
|
||||||
this.$parent.notification.sendgridSubject = "Notification from Your Uptime Kuma";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@@ -4,20 +4,11 @@
|
|||||||
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
||||||
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
|
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
|
||||||
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
||||||
<div class="form-text">
|
|
||||||
{{ $t("aboutSlackUsername") }}
|
|
||||||
</div>
|
|
||||||
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
|
||||||
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
||||||
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
|
||||||
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
||||||
|
|
||||||
<label class="form-label">{{ $t("Message format") }}</label>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input id="slack-text-message" v-model="$parent.notification.slackrichmessage" type="checkbox" class="form-check-input">
|
|
||||||
<label for="slack-text-message" class="form-label">{{ $t("Send rich messages") }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user