commit
e06c3ee5d4
@ -0,0 +1,28 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
projectId: "vyjuem",
|
||||||
|
e2e: {
|
||||||
|
experimentalStudio: true,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
|
||||||
|
},
|
||||||
|
fixturesFolder: "test/cypress/fixtures",
|
||||||
|
screenshotsFolder: "test/cypress/screenshots",
|
||||||
|
videosFolder: "test/cypress/videos",
|
||||||
|
downloadsFolder: "test/cypress/downloads",
|
||||||
|
supportFile: "test/cypress/support/e2e.js",
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/e2e/setup.cy.js",
|
||||||
|
"test/cypress/e2e/**/*.js"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,10 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
supportFile: false,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/unit/**/*.js"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
@ -1,33 +0,0 @@
|
|||||||
const PuppeteerEnvironment = require("jest-environment-puppeteer");
|
|
||||||
const util = require("util");
|
|
||||||
|
|
||||||
class DebugEnv extends PuppeteerEnvironment {
|
|
||||||
async handleTestEvent(event, state) {
|
|
||||||
const ignoredEvents = [
|
|
||||||
"setup",
|
|
||||||
"add_hook",
|
|
||||||
"start_describe_definition",
|
|
||||||
"add_test",
|
|
||||||
"finish_describe_definition",
|
|
||||||
"run_start",
|
|
||||||
"run_describe_start",
|
|
||||||
"test_start",
|
|
||||||
"hook_start",
|
|
||||||
"hook_success",
|
|
||||||
"test_fn_start",
|
|
||||||
"test_fn_success",
|
|
||||||
"test_done",
|
|
||||||
"run_describe_finish",
|
|
||||||
"run_finish",
|
|
||||||
"teardown",
|
|
||||||
"test_fn_failure",
|
|
||||||
];
|
|
||||||
if (!ignoredEvents.includes(event.name)) {
|
|
||||||
console.log(
|
|
||||||
new Date().toString() + ` Unhandled event [${event.name}] ` + util.inspect(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DebugEnv;
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"rootDir": "..",
|
|
||||||
"testRegex": "./test/frontend.spec.js",
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"launch": {
|
|
||||||
"dumpio": true,
|
|
||||||
"slowMo": 500,
|
|
||||||
"headless": process.env.HEADLESS_TEST || false,
|
|
||||||
"userDataDir": "./data/test-chrome-profile",
|
|
||||||
args: [
|
|
||||||
"--disable-setuid-sandbox",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-dev-shm-usage",
|
|
||||||
"--no-default-browser-check",
|
|
||||||
"--no-experiments",
|
|
||||||
"--no-first-run",
|
|
||||||
"--no-pings",
|
|
||||||
"--no-sandbox",
|
|
||||||
"--no-zygote",
|
|
||||||
"--single-process",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"verbose": true,
|
|
||||||
"preset": "jest-puppeteer",
|
|
||||||
"globals": {
|
|
||||||
"__DEV__": true
|
|
||||||
},
|
|
||||||
"testRegex": "./test/e2e.spec.js",
|
|
||||||
"testEnvironment": "./config/jest-debug-env.js",
|
|
||||||
"rootDir": "..",
|
|
||||||
"testTimeout": 30000,
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from "cypress";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
baseUrl: "http://localhost:3002",
|
|
||||||
defaultCommandTimeout: 10000,
|
|
||||||
pageLoadTimeout: 60000,
|
|
||||||
viewportWidth: 1920,
|
|
||||||
viewportHeight: 1080,
|
|
||||||
specPattern: ["cypress/e2e/setup.cy.ts", "cypress/e2e/**/*.ts"],
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
baseUrl: "http://localhost:3002",
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,24 +0,0 @@
|
|||||||
import { actor } from "../support/actors/actor";
|
|
||||||
import { DEFAULT_USER_DATA } from "../support/const/user-data";
|
|
||||||
import { DashboardPage } from "../support/pages/dasboard-page";
|
|
||||||
import { SetupPage } from "../support/pages/setup-page";
|
|
||||||
|
|
||||||
describe("user can create a new account on setup page", () => {
|
|
||||||
before(() => {
|
|
||||||
cy.visit("/setup");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("user can create new account", () => {
|
|
||||||
cy.url().should("be.equal", SetupPage.url);
|
|
||||||
actor.setupTask.fillAndSubmitSetupForm(
|
|
||||||
DEFAULT_USER_DATA.username,
|
|
||||||
DEFAULT_USER_DATA.password,
|
|
||||||
DEFAULT_USER_DATA.password
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.url().should("be.equal", DashboardPage.url);
|
|
||||||
cy.get('[role="alert"]')
|
|
||||||
.should("be.visible")
|
|
||||||
.and("contain.text", "Added Successfully.");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,8 +0,0 @@
|
|||||||
import { SetupTask } from "../tasks/setup-task";
|
|
||||||
|
|
||||||
class Actor {
|
|
||||||
setupTask: SetupTask = new SetupTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
const actor = new Actor();
|
|
||||||
export { actor };
|
|
@ -1 +0,0 @@
|
|||||||
import "./commands";
|
|
@ -1,15 +0,0 @@
|
|||||||
import { SetupPage } from "../pages/setup-page";
|
|
||||||
|
|
||||||
export class SetupTask {
|
|
||||||
fillAndSubmitSetupForm(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
passwordRepeat: string
|
|
||||||
) {
|
|
||||||
cy.get(SetupPage.usernameInput).type(username);
|
|
||||||
cy.get(SetupPage.passWordInput).type(password);
|
|
||||||
cy.get(SetupPage.passwordRepeatInput).type(passwordRepeat);
|
|
||||||
|
|
||||||
cy.get(SetupPage.submitSetupForm).click();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,5 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD game VARCHAR(255);
|
||||||
|
COMMIT
|
@ -0,0 +1,25 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_url VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_protobuf TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_body TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_metadata TEXT default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_method VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_service_name VARCHAR(255) default null;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD grpc_enable_tls BOOLEAN default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,83 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
|
||||||
|
DROP TABLE IF EXISTS maintenance_status_page;
|
||||||
|
DROP TABLE IF EXISTS monitor_maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance;
|
||||||
|
DROP TABLE IF EXISTS maintenance_timeslot;
|
||||||
|
|
||||||
|
-- maintenance
|
||||||
|
CREATE TABLE [maintenance] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[title] VARCHAR(150) NOT NULL,
|
||||||
|
[description] TEXT NOT NULL,
|
||||||
|
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
[active] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
|
||||||
|
[start_date] DATETIME,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[start_time] TIME,
|
||||||
|
[end_time] TIME,
|
||||||
|
[weekdays] VARCHAR2(250) DEFAULT '[]',
|
||||||
|
[days_of_month] TEXT DEFAULT '[]',
|
||||||
|
[interval_day] INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [manual_active] ON [maintenance] (
|
||||||
|
[strategy],
|
||||||
|
[active]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [active] ON [maintenance] ([active]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
|
||||||
|
|
||||||
|
-- maintenance_status_page
|
||||||
|
CREATE TABLE maintenance_status_page (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
status_page_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [status_page_id_index]
|
||||||
|
ON [maintenance_status_page]([status_page_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index]
|
||||||
|
ON [maintenance_status_page]([maintenance_id]);
|
||||||
|
|
||||||
|
-- maintenance_timeslot
|
||||||
|
CREATE TABLE [maintenance_timeslot] (
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[start_date] DATETIME NOT NULL,
|
||||||
|
[end_date] DATETIME,
|
||||||
|
[generated_next] BOOLEAN DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
|
||||||
|
|
||||||
|
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
|
||||||
|
[maintenance_id] DESC,
|
||||||
|
[start_date] DESC,
|
||||||
|
[end_date] DESC
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
|
||||||
|
|
||||||
|
-- monitor_maintenance
|
||||||
|
CREATE TABLE monitor_maintenance (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
maintenance_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
|
||||||
|
|
||||||
|
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,5 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD packet_size INTEGER DEFAULT 56 NOT NULL;
|
||||||
|
COMMIT;
|
@ -0,0 +1,16 @@
|
|||||||
|
############################################
|
||||||
|
# Build in Golang
|
||||||
|
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
|
||||||
|
############################################
|
||||||
|
FROM golang:1.19-buster
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
COPY ./extra/ ./extra/
|
||||||
|
|
||||||
|
# Compile healthcheck.go
|
||||||
|
RUN apt update && \
|
||||||
|
apt --yes --no-install-recommends install curl && \
|
||||||
|
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
|
||||||
|
apt --yes --no-install-recommends install nodejs && \
|
||||||
|
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
|
||||||
|
apt --yes remove nodejs
|
@ -0,0 +1,13 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
uptime-kuma:
|
||||||
|
container_name: uptime-kuma-dev
|
||||||
|
image: louislam/uptime-kuma:nightly2
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ../server:/app/server
|
||||||
|
ports:
|
||||||
|
- "3001:3001" # <Host Port>:<Container Port>
|
||||||
|
- "3307:3306"
|
||||||
|
|
@ -1,14 +1,15 @@
|
|||||||
# Simple docker-compose.yml
|
version: '3.8'
|
||||||
# You can change your port or volume location
|
|
||||||
|
|
||||||
version: '3.3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
uptime-kuma:
|
uptime-kuma:
|
||||||
image: louislam/uptime-kuma:1
|
image: louislam/uptime-kuma:2
|
||||||
container_name: uptime-kuma
|
container_name: uptime-kuma
|
||||||
volumes:
|
volumes:
|
||||||
- ./uptime-kuma-data:/app/data
|
- uptime-kuma:/app/data
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001 # <Host Port>:<Container Port>
|
- "3001:3001" # <Host Port>:<Container Port>
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
uptime-kuma:
|
||||||
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const platform = process.argv[2];
|
||||||
|
|
||||||
|
if (!platform) {
|
||||||
|
console.error("No platform??");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "linux/arm/v7") {
|
||||||
|
console.log("Arch: armv7");
|
||||||
|
if (fs.existsSync("./extra/healthcheck-armv7")) {
|
||||||
|
fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
|
||||||
|
console.log("Already built in the host, skip.");
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fs.existsSync("./extra/healthcheck-armv7")) {
|
||||||
|
fs.rmSync("./extra/healthcheck-armv7");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
|
||||||
|
console.log(output);
|
||||||
|
|
@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# set -e Exit the script if an error happens
|
|
||||||
set -e
|
|
||||||
PUID=${PUID=0}
|
|
||||||
PGID=${PGID=0}
|
|
||||||
|
|
||||||
files_ownership () {
|
|
||||||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
|
||||||
# -R Recursively descends the specified directories
|
|
||||||
# -c Like verbose but report only when a change is made
|
|
||||||
chown -hRc "$PUID":"$PGID" /app/data
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "==> Performing startup jobs and maintenance tasks"
|
|
||||||
files_ownership
|
|
||||||
|
|
||||||
echo "==> Starting application with user $PUID group $PGID"
|
|
||||||
|
|
||||||
# --clear-groups Clear supplementary groups.
|
|
||||||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"
|
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* If changed, have to run `npm run build-docker-builder-go`.
|
||||||
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
isFreeBSD := runtime.GOOS == "freebsd"
|
||||||
|
|
||||||
|
// Is K8S + uptime-kuma as the container name
|
||||||
|
// See #2083
|
||||||
|
isK8s := strings.HasPrefix(os.Getenv("UPTIME_KUMA_PORT"), "tcp://")
|
||||||
|
|
||||||
|
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 28 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY")
|
||||||
|
if len(sslKey) == 0 {
|
||||||
|
sslKey = os.Getenv("SSL_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT")
|
||||||
|
if len(sslCert) == 0 {
|
||||||
|
sslCert = os.Getenv("SSL_CERT")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := os.Getenv("UPTIME_KUMA_HOST")
|
||||||
|
if len(hostname) == 0 && !isFreeBSD {
|
||||||
|
hostname = os.Getenv("HOST")
|
||||||
|
}
|
||||||
|
if len(hostname) == 0 {
|
||||||
|
hostname = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := ""
|
||||||
|
// UPTIME_KUMA_PORT is override by K8S unexpectedly,
|
||||||
|
if !isK8s {
|
||||||
|
port = os.Getenv("UPTIME_KUMA_PORT")
|
||||||
|
}
|
||||||
|
if len(port) == 0 {
|
||||||
|
port = os.Getenv("PORT")
|
||||||
|
}
|
||||||
|
if len(port) == 0 {
|
||||||
|
port = "3001"
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := ""
|
||||||
|
if len(sslKey) != 0 && len(sslCert) != 0 {
|
||||||
|
protocol = "https"
|
||||||
|
} else {
|
||||||
|
protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
url := protocol + "://" + hostname + ":" + port
|
||||||
|
|
||||||
|
log.Println("Checking " + url)
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,157 @@
|
|||||||
|
const { log } = require("../src/util");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is only used inside the docker container
|
||||||
|
*/
|
||||||
|
class EmbeddedMariaDB {
|
||||||
|
|
||||||
|
static instance = null;
|
||||||
|
|
||||||
|
exec = "mariadbd";
|
||||||
|
|
||||||
|
mariadbDataDir = "/app/data/mariadb";
|
||||||
|
|
||||||
|
runDir = "/app/data/run/mariadb";
|
||||||
|
|
||||||
|
socketPath = this.runDir + "/mysqld.sock";
|
||||||
|
|
||||||
|
childProcess = null;
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
started = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {EmbeddedMariaDB}
|
||||||
|
*/
|
||||||
|
static getInstance() {
|
||||||
|
if (!EmbeddedMariaDB.instance) {
|
||||||
|
EmbeddedMariaDB.instance = new EmbeddedMariaDB();
|
||||||
|
}
|
||||||
|
return EmbeddedMariaDB.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static hasInstance() {
|
||||||
|
return !!EmbeddedMariaDB.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (this.childProcess) {
|
||||||
|
log.info("mariadb", "Already started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initDB();
|
||||||
|
|
||||||
|
this.running = true;
|
||||||
|
log.info("mariadb", "Starting Embedded MariaDB");
|
||||||
|
this.childProcess = childProcess.spawn(this.exec, [
|
||||||
|
"--user=node",
|
||||||
|
"--datadir=" + this.mariadbDataDir,
|
||||||
|
`--socket=${this.socketPath}`,
|
||||||
|
`--pid-file=${this.runDir}/mysqld.pid`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.childProcess.on("close", (code) => {
|
||||||
|
this.running = false;
|
||||||
|
this.childProcess = null;
|
||||||
|
this.started = false;
|
||||||
|
log.info("mariadb", "Stopped Embedded MariaDB: " + code);
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.childProcess.on("error", (err) => {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
log.error("mariadb", `Embedded MariaDB: ${this.exec} is not found`);
|
||||||
|
} else {
|
||||||
|
log.error("mariadb", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let handler = (data) => {
|
||||||
|
log.debug("mariadb", data.toString("utf-8"));
|
||||||
|
if (data.toString("utf-8").includes("ready for connections")) {
|
||||||
|
this.initDBAfterStarted();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.childProcess.stdout.on("data", handler);
|
||||||
|
this.childProcess.stderr.on("data", handler);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let interval = setInterval(() => {
|
||||||
|
if (this.started) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.childProcess) {
|
||||||
|
this.childProcess.kill("SIGINT");
|
||||||
|
this.childProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initDB() {
|
||||||
|
if (!fs.existsSync(this.mariadbDataDir)) {
|
||||||
|
log.info("mariadb", `Embedded MariaDB: ${this.mariadbDataDir} is not found, create one now.`);
|
||||||
|
fs.mkdirSync(this.mariadbDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = childProcess.spawnSync("mysql_install_db", [
|
||||||
|
"--user=node",
|
||||||
|
"--ldata=" + this.mariadbDataDir,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
let error = result.stderr.toString("utf-8");
|
||||||
|
log.error("mariadb", error);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log.info("mariadb", "Embedded MariaDB: mysql_install_db done:" + result.stdout.toString("utf-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.runDir)) {
|
||||||
|
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
|
||||||
|
fs.mkdirSync(this.runDir, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async initDBAfterStarted() {
|
||||||
|
const connection = mysql.createConnection({
|
||||||
|
socketPath: this.socketPath,
|
||||||
|
user: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");
|
||||||
|
log.debug("mariadb", "CREATE DATABASE: " + JSON.stringify(result));
|
||||||
|
|
||||||
|
log.info("mariadb", "Embedded MariaDB is ready for connections");
|
||||||
|
this.started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
EmbeddedMariaDB,
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
class Git {
|
||||||
|
|
||||||
|
static clone(repoURL, cwd, targetDir = ".") {
|
||||||
|
let result = childProcess.spawnSync("git", [
|
||||||
|
"clone",
|
||||||
|
repoURL,
|
||||||
|
targetDir,
|
||||||
|
], {
|
||||||
|
cwd: cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(result.stderr.toString("utf-8"));
|
||||||
|
} else {
|
||||||
|
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Git,
|
||||||
|
};
|
@ -0,0 +1,240 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||||
|
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
class Maintenance extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
|
||||||
|
let dateRange = [];
|
||||||
|
if (this.start_date) {
|
||||||
|
dateRange.push(utcToLocal(this.start_date));
|
||||||
|
if (this.end_date) {
|
||||||
|
dateRange.push(utcToLocal(this.end_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeRange = [];
|
||||||
|
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||||
|
timeRange.push(startTime);
|
||||||
|
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||||
|
timeRange.push(endTime);
|
||||||
|
|
||||||
|
let obj = {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
strategy: this.strategy,
|
||||||
|
intervalDay: this.interval_day,
|
||||||
|
active: !!this.active,
|
||||||
|
dateRange: dateRange,
|
||||||
|
timeRange: timeRange,
|
||||||
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||||
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||||
|
timeslotList: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeslotList = await this.getTimeslotList();
|
||||||
|
|
||||||
|
for (let timeslot of timeslotList) {
|
||||||
|
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.weekdays)) {
|
||||||
|
obj.weekdays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.daysOfMonth)) {
|
||||||
|
obj.daysOfMonth = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintenance Status
|
||||||
|
if (!obj.active) {
|
||||||
|
obj.status = "inactive";
|
||||||
|
} else if (obj.strategy === "manual") {
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
} else if (obj.timeslotList.length > 0) {
|
||||||
|
let currentTimestamp = dayjs().unix();
|
||||||
|
|
||||||
|
for (let timeslot of obj.timeslotList) {
|
||||||
|
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
|
||||||
|
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
|
||||||
|
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
|
||||||
|
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
|
||||||
|
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
|
||||||
|
|
||||||
|
obj.status = "under-maintenance";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.status) {
|
||||||
|
obj.status = "scheduled";
|
||||||
|
}
|
||||||
|
} else if (obj.timeslotList.length === 0) {
|
||||||
|
obj.status = "ended";
|
||||||
|
} else {
|
||||||
|
obj.status = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only get future or current timeslots only
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async getTimeslotList() {
|
||||||
|
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||||
|
SELECT maintenance_timeslot.*
|
||||||
|
FROM maintenance_timeslot, maintenance
|
||||||
|
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||||
|
AND maintenance.id = ?
|
||||||
|
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||||
|
`, [
|
||||||
|
this.id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON(timezone = null) {
|
||||||
|
return this.toPublicJSON(timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of weekdays that the maintenance is active for
|
||||||
|
* Monday=1, Tuesday=2 etc.
|
||||||
|
* @returns {number[]} Array of active weekdays
|
||||||
|
*/
|
||||||
|
getDayOfWeekList() {
|
||||||
|
log.debug("timeslot", "List: " + this.weekdays);
|
||||||
|
return JSON.parse(this.weekdays).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of days in month that maintenance is active for
|
||||||
|
* @returns {number[]} Array of active days in month
|
||||||
|
*/
|
||||||
|
getDayOfMonthList() {
|
||||||
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the start date and time for maintenance
|
||||||
|
* @returns {dayjs.Dayjs} Start date and time
|
||||||
|
*/
|
||||||
|
getStartDateTime() {
|
||||||
|
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||||
|
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||||
|
|
||||||
|
// Start Time
|
||||||
|
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
|
||||||
|
log.debug("timeslot", "startTime: " + startTimeSecond);
|
||||||
|
|
||||||
|
// Bake StartDate + StartTime = Start DateTime
|
||||||
|
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the duraction of maintenance in seconds
|
||||||
|
* @returns {number} Duration of maintenance
|
||||||
|
*/
|
||||||
|
getDuration() {
|
||||||
|
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||||
|
// Add 24hours if it is across day
|
||||||
|
if (duration < 0) {
|
||||||
|
duration += 24 * 3600;
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert data from socket to bean
|
||||||
|
* @param {Bean} bean Bean to fill in
|
||||||
|
* @param {Object} obj Data to fill bean with
|
||||||
|
* @returns {Bean} Filled bean
|
||||||
|
*/
|
||||||
|
static jsonToBean(bean, obj) {
|
||||||
|
if (obj.id) {
|
||||||
|
bean.id = obj.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||||
|
if (obj.timeRange[0]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[0]);
|
||||||
|
if (obj.timeRange[1]) {
|
||||||
|
timeObjectToUTC(obj.timeRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.title = obj.title;
|
||||||
|
bean.description = obj.description;
|
||||||
|
bean.strategy = obj.strategy;
|
||||||
|
bean.interval_day = obj.intervalDay;
|
||||||
|
bean.active = obj.active;
|
||||||
|
|
||||||
|
if (obj.dateRange[0]) {
|
||||||
|
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||||
|
|
||||||
|
if (obj.dateRange[1]) {
|
||||||
|
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||||
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||||
|
|
||||||
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||||
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL conditions for active maintenance
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getActiveMaintenanceSQLCondition() {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||||
|
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1)
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL conditions for active and future maintenance
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||||
|
AND maintenance.active = 1)
|
||||||
|
OR
|
||||||
|
(maintenance.strategy = 'manual' AND active = 1))
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Maintenance;
|
@ -0,0 +1,198 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
|
||||||
|
class MaintenanceTimeslot extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: this.id,
|
||||||
|
startDate: this.start_date,
|
||||||
|
endDate: this.end_date,
|
||||||
|
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||||
|
serverTimezoneOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
async toJSON() {
|
||||||
|
return await this.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Maintenance} maintenance
|
||||||
|
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
|
||||||
|
* @param {boolean} removeExist Remove existing timeslot before create
|
||||||
|
* @returns {Promise<MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
|
||||||
|
if (removeExist) {
|
||||||
|
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
|
||||||
|
maintenance.id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maintenance.strategy === "manual") {
|
||||||
|
log.debug("maintenance", "No need to generate timeslot for manual type");
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "single") {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = maintenance.start_date;
|
||||||
|
bean.end_date = maintenance.end_date;
|
||||||
|
bean.generated_next = true;
|
||||||
|
return await R.store(bean);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-interval") {
|
||||||
|
// Prevent dead loop, in case interval_day is not set
|
||||||
|
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
|
||||||
|
maintenance.interval_day = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
return startDateTime.add(maintenance.interval_day, "day");
|
||||||
|
}, () => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-weekday") {
|
||||||
|
let dayOfWeekList = maintenance.getDayOfWeekList();
|
||||||
|
log.debug("timeslot", dayOfWeekList);
|
||||||
|
|
||||||
|
if (dayOfWeekList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No weekdays selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
log.debug("timeslot", "nextDateTime: " + startDateTime);
|
||||||
|
|
||||||
|
let day = startDateTime.local().day();
|
||||||
|
log.debug("timeslot", "nextDateTime.day(): " + day);
|
||||||
|
|
||||||
|
return dayOfWeekList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
|
||||||
|
} else if (maintenance.strategy === "recurring-day-of-month") {
|
||||||
|
let dayOfMonthList = maintenance.getDayOfMonthList();
|
||||||
|
if (dayOfMonthList.length <= 0) {
|
||||||
|
log.debug("timeslot", "No day selected?");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = (startDateTime) => {
|
||||||
|
let day = parseInt(startDateTime.local().format("D"));
|
||||||
|
|
||||||
|
log.debug("timeslot", "day: " + day);
|
||||||
|
|
||||||
|
// Check 1-31
|
||||||
|
if (dayOfMonthList.includes(day)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check "lastDay1","lastDay2"...
|
||||||
|
let daysInMonth = startDateTime.daysInMonth();
|
||||||
|
let lastDayList = [];
|
||||||
|
|
||||||
|
// Small first, e.g. 28 > 29 > 30 > 31
|
||||||
|
for (let i = 4; i >= 1; i--) {
|
||||||
|
if (dayOfMonthList.includes("lastDay" + i)) {
|
||||||
|
lastDayList.push(daysInMonth - i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("timeslot", lastDayList);
|
||||||
|
return lastDayList.includes(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||||
|
while (true) {
|
||||||
|
startDateTime = startDateTime.add(1, "day");
|
||||||
|
if (isValid(startDateTime)) {
|
||||||
|
return startDateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isValid);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown maintenance strategy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a next timeslot for all recurring types
|
||||||
|
* @param maintenance
|
||||||
|
* @param minDate
|
||||||
|
* @param {function} nextDayCallback The logic how to get the next possible day
|
||||||
|
* @param {function} isValidCallback Check the day whether is matched the current strategy
|
||||||
|
* @returns {Promise<null|MaintenanceTimeslot>}
|
||||||
|
*/
|
||||||
|
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
|
||||||
|
let bean = R.dispense("maintenance_timeslot");
|
||||||
|
|
||||||
|
let duration = maintenance.getDuration();
|
||||||
|
let startDateTime = maintenance.getStartDateTime();
|
||||||
|
let endDateTime;
|
||||||
|
|
||||||
|
// Keep generating from the first possible date, until it is ok
|
||||||
|
while (true) {
|
||||||
|
log.debug("timeslot", "startDateTime: " + startDateTime.format());
|
||||||
|
|
||||||
|
// Handling out of effective date range
|
||||||
|
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
log.debug("timeslot", "Out of effective date range");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
endDateTime = startDateTime.add(duration, "second");
|
||||||
|
|
||||||
|
// If endDateTime is out of effective date range, use the end datetime from effective date range
|
||||||
|
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||||
|
endDateTime = dayjs.utc(maintenance.end_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If minDate is set, the endDateTime must be bigger than it.
|
||||||
|
// And the endDateTime must be bigger current time
|
||||||
|
// Is valid under current recurring strategy
|
||||||
|
if (
|
||||||
|
(!minDate || endDateTime.diff(minDate) > 0) &&
|
||||||
|
endDateTime.diff(dayjs()) > 0 &&
|
||||||
|
isValidCallback(startDateTime)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startDateTime = nextDayCallback(startDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.maintenance_id = maintenance.id;
|
||||||
|
bean.start_date = localToUTC(startDateTime);
|
||||||
|
bean.end_date = localToUTC(endDateTime);
|
||||||
|
bean.generated_next = false;
|
||||||
|
return await R.store(bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MaintenanceTimeslot;
|
@ -0,0 +1,20 @@
|
|||||||
|
import { PluginFunc, ConfigType } from 'dayjs'
|
||||||
|
|
||||||
|
declare const plugin: PluginFunc
|
||||||
|
export = plugin
|
||||||
|
|
||||||
|
declare module 'dayjs' {
|
||||||
|
interface Dayjs {
|
||||||
|
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
|
||||||
|
offsetName(type?: 'short' | 'long'): string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DayjsTimezone {
|
||||||
|
(date: ConfigType, timezone?: string): Dayjs
|
||||||
|
(date: ConfigType, format: string, timezone?: string): Dayjs
|
||||||
|
guess(): string
|
||||||
|
setDefault(timezone?: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const tz: DayjsTimezone
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Copy from node_modules/dayjs/plugin/timezone.js
|
||||||
|
* Try to fix https://github.com/louislam/uptime-kuma/issues/2318
|
||||||
|
* Source: https://github.com/iamkun/dayjs/tree/dev/src/plugin/utc
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
!function (t, e) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
typeof exports == "object" && typeof module != "undefined" ? module.exports = e() : typeof define == "function" && define.amd ? define(e) : (t = typeof globalThis != "undefined" ? globalThis : t || self).dayjs_plugin_timezone = e();
|
||||||
|
}(this, (function () {
|
||||||
|
"use strict";
|
||||||
|
let t = {
|
||||||
|
year: 0,
|
||||||
|
month: 1,
|
||||||
|
day: 2,
|
||||||
|
hour: 3,
|
||||||
|
minute: 4,
|
||||||
|
second: 5
|
||||||
|
};
|
||||||
|
let e = {};
|
||||||
|
return function (n, i, o) {
|
||||||
|
let r;
|
||||||
|
let a = function (t, n, i) {
|
||||||
|
void 0 === i && (i = {});
|
||||||
|
let o = new Date(t);
|
||||||
|
let r = function (t, n) {
|
||||||
|
void 0 === n && (n = {});
|
||||||
|
let i = n.timeZoneName || "short";
|
||||||
|
let o = t + "|" + i;
|
||||||
|
let r = e[o];
|
||||||
|
return r || (r = new Intl.DateTimeFormat("en-US", {
|
||||||
|
hour12: !1,
|
||||||
|
timeZone: t,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZoneName: i
|
||||||
|
}), e[o] = r), r;
|
||||||
|
}(n, i);
|
||||||
|
return r.formatToParts(o);
|
||||||
|
};
|
||||||
|
let u = function (e, n) {
|
||||||
|
let i = a(e, n);
|
||||||
|
let r = [];
|
||||||
|
let u = 0;
|
||||||
|
for (; u < i.length; u += 1) {
|
||||||
|
let f = i[u];
|
||||||
|
let s = f.type;
|
||||||
|
let m = f.value;
|
||||||
|
let c = t[s];
|
||||||
|
c >= 0 && (r[c] = parseInt(m, 10));
|
||||||
|
}
|
||||||
|
let d = r[3];
|
||||||
|
let l = d === 24 ? 0 : d;
|
||||||
|
let v = r[0] + "-" + r[1] + "-" + r[2] + " " + l + ":" + r[4] + ":" + r[5] + ":000";
|
||||||
|
let h = +e;
|
||||||
|
return (o.utc(v).valueOf() - (h -= h % 1e3)) / 6e4;
|
||||||
|
};
|
||||||
|
let f = i.prototype;
|
||||||
|
f.tz = function (t, e) {
|
||||||
|
void 0 === t && (t = r);
|
||||||
|
let n = this.utcOffset();
|
||||||
|
let i = this.toDate();
|
||||||
|
let a = i.toLocaleString("en-US", { timeZone: t }).replace("\u202f", " ");
|
||||||
|
let u = Math.round((i - new Date(a)) / 1e3 / 60);
|
||||||
|
let f = o(a).$set("millisecond", this.$ms).utcOffset(15 * -Math.round(i.getTimezoneOffset() / 15) - u, !0);
|
||||||
|
if (e) {
|
||||||
|
let s = f.utcOffset();
|
||||||
|
f = f.add(n - s, "minute");
|
||||||
|
}
|
||||||
|
return f.$x.$timezone = t, f;
|
||||||
|
}, f.offsetName = function (t) {
|
||||||
|
let e = this.$x.$timezone || o.tz.guess();
|
||||||
|
let n = a(this.valueOf(), e, { timeZoneName: t }).find((function (t) {
|
||||||
|
return t.type.toLowerCase() === "timezonename";
|
||||||
|
}));
|
||||||
|
return n && n.value;
|
||||||
|
};
|
||||||
|
let s = f.startOf;
|
||||||
|
f.startOf = function (t, e) {
|
||||||
|
if (!this.$x || !this.$x.$timezone) {
|
||||||
|
return s.call(this, t, e);
|
||||||
|
}
|
||||||
|
let n = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
|
||||||
|
return s.call(n, t, e).tz(this.$x.$timezone, !0);
|
||||||
|
}, o.tz = function (t, e, n) {
|
||||||
|
let i = n && e;
|
||||||
|
let a = n || e || r;
|
||||||
|
let f = u(+o(), a);
|
||||||
|
if (typeof t != "string") {
|
||||||
|
return o(t).tz(a);
|
||||||
|
}
|
||||||
|
let s = function (t, e, n) {
|
||||||
|
let i = t - 60 * e * 1e3;
|
||||||
|
let o = u(i, n);
|
||||||
|
if (e === o) {
|
||||||
|
return [ i, e ];
|
||||||
|
}
|
||||||
|
let r = u(i -= 60 * (o - e) * 1e3, n);
|
||||||
|
return o === r ? [ i, o ] : [ t - 60 * Math.min(o, r) * 1e3, Math.max(o, r) ];
|
||||||
|
}(o.utc(t, i).valueOf(), f, a);
|
||||||
|
let m = s[0];
|
||||||
|
let c = s[1];
|
||||||
|
let d = o(m).utcOffset(c);
|
||||||
|
return d.$x.$timezone = a, d;
|
||||||
|
}, o.tz.guess = function () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}, o.tz.setDefault = function (t) {
|
||||||
|
r = t;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
@ -0,0 +1,19 @@
|
|||||||
|
class MonitorType {
|
||||||
|
|
||||||
|
name = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Monitor} monitor
|
||||||
|
* @param {Heartbeat} heartbeat
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat) {
|
||||||
|
throw new Error("You need to override check()");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MonitorType,
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class FreeMobile extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "FreeMobile";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
try {
|
||||||
|
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||||
|
"user": notification.freemobileUser,
|
||||||
|
"pass": notification.freemobilePass,
|
||||||
|
});
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FreeMobile;
|
@ -0,0 +1,31 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class Kook extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "Kook";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
let url = "https://www.kookapp.cn/api/v3/message/create";
|
||||||
|
let data = {
|
||||||
|
target_id: notification.kookGuildID,
|
||||||
|
content: msg,
|
||||||
|
};
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bot " + notification.kookBotToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await axios.post(url, data, config);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Kook;
|
@ -0,0 +1,71 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
class SMSEagle extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "SMSEagle";
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let postData;
|
||||||
|
let sendMethod;
|
||||||
|
let recipientType;
|
||||||
|
|
||||||
|
let encoding = (notification.smseagleEncoding) ? "1" : "0";
|
||||||
|
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
|
||||||
|
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-contact") {
|
||||||
|
recipientType = "contactname";
|
||||||
|
sendMethod = "sms.send_tocontact";
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-group") {
|
||||||
|
recipientType = "groupname";
|
||||||
|
sendMethod = "sms.send_togroup";
|
||||||
|
}
|
||||||
|
if (notification.smseagleRecipientType === "smseagle-to") {
|
||||||
|
recipientType = "to";
|
||||||
|
sendMethod = "sms.send_sms";
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
access_token: notification.smseagleToken,
|
||||||
|
[recipientType]: notification.smseagleRecipient,
|
||||||
|
message: msg,
|
||||||
|
responsetype: "extended",
|
||||||
|
unicode: encoding,
|
||||||
|
highpriority: priority
|
||||||
|
};
|
||||||
|
|
||||||
|
postData = {
|
||||||
|
method: sendMethod,
|
||||||
|
params: params
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
|
||||||
|
|
||||||
|
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
|
||||||
|
let error = "";
|
||||||
|
if (resp.data.result && resp.data.result.error_text) {
|
||||||
|
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
|
||||||
|
} else {
|
||||||
|
error = "SMSEagle API returned an unexpected response";
|
||||||
|
}
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SMSEagle;
|
@ -0,0 +1,113 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||||
|
const { setting } = require("../util-server");
|
||||||
|
let successMessage = "Sent Successfully.";
|
||||||
|
|
||||||
|
class Splunk extends NotificationProvider {
|
||||||
|
name = "Splunk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
const title = "Uptime Kuma Alert";
|
||||||
|
const monitor = {
|
||||||
|
type: "ping",
|
||||||
|
url: "Uptime Kuma Test Button",
|
||||||
|
};
|
||||||
|
return this.postNotification(notification, title, msg, monitor, "trigger");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === UP) {
|
||||||
|
const title = "Uptime Kuma Monitor ✅ Up";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON.status === DOWN) {
|
||||||
|
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||||
|
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if result is successful, result code should be in range 2xx
|
||||||
|
* @param {Object} result Axios response object
|
||||||
|
* @throws {Error} The status code is not in range 2xx
|
||||||
|
*/
|
||||||
|
checkResult(result) {
|
||||||
|
if (result.status == null) {
|
||||||
|
throw new Error("Splunk notification failed with invalid response!");
|
||||||
|
}
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
throw new Error("Splunk notification failed with status code " + result.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message
|
||||||
|
* @param {BeanModel} notification Message title
|
||||||
|
* @param {string} title Message title
|
||||||
|
* @param {string} body Message
|
||||||
|
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||||
|
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||||
|
|
||||||
|
let monitorUrl;
|
||||||
|
if (monitorInfo.type === "port") {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
if (monitorInfo.port) {
|
||||||
|
monitorUrl += ":" + monitorInfo.port;
|
||||||
|
}
|
||||||
|
} else if (monitorInfo.hostname != null) {
|
||||||
|
monitorUrl = monitorInfo.hostname;
|
||||||
|
} else {
|
||||||
|
monitorUrl = monitorInfo.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventAction === "recovery") {
|
||||||
|
if (notification.splunkAutoResolve === "0") {
|
||||||
|
return "No action required";
|
||||||
|
}
|
||||||
|
eventAction = notification.splunkAutoResolve;
|
||||||
|
} else {
|
||||||
|
eventAction = notification.splunkSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
url: notification.splunkRestURL,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: {
|
||||||
|
message_type: eventAction,
|
||||||
|
state_message: `[${title}] [${monitorUrl}] ${body}`,
|
||||||
|
entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name,
|
||||||
|
routing_key: notification.pagerdutyIntegrationKey,
|
||||||
|
entity_id: "Uptime Kuma/" + monitorInfo.id,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
if (baseURL && monitorInfo) {
|
||||||
|
options.client = "Uptime Kuma";
|
||||||
|
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await axios.request(options);
|
||||||
|
this.checkResult(result);
|
||||||
|
if (result.statusText != null) {
|
||||||
|
return "Splunk notification succeed: " + result.statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Splunk;
|
@ -0,0 +1,116 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class ZohoCliq extends NotificationProvider {
|
||||||
|
|
||||||
|
name = "ZohoCliq";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the message to send
|
||||||
|
* @param {const} status The status constant
|
||||||
|
* @param {string} monitorName Name of monitor
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_statusMessageFactory = (status, monitorName) => {
|
||||||
|
if (status === DOWN) {
|
||||||
|
return `🔴 Application [${monitorName}] went down\n`;
|
||||||
|
} else if (status === UP) {
|
||||||
|
return `✅ Application [${monitorName}] is back online\n`;
|
||||||
|
}
|
||||||
|
return "Notification\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the notification
|
||||||
|
* @param {string} webhookUrl URL to send the request to
|
||||||
|
* @param {Array} payload Payload generated by _notificationPayloadFactory
|
||||||
|
*/
|
||||||
|
_sendNotification = async (webhookUrl, payload) => {
|
||||||
|
await axios.post(webhookUrl, { text: payload.join("\n") });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate payload for notification
|
||||||
|
* @param {const} status The status of the monitor
|
||||||
|
* @param {string} monitorMessage Message to send
|
||||||
|
* @param {string} monitorName Name of monitor affected
|
||||||
|
* @param {string} monitorUrl URL of monitor affected
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
_notificationPayloadFactory = ({
|
||||||
|
status,
|
||||||
|
monitorMessage,
|
||||||
|
monitorName,
|
||||||
|
monitorUrl,
|
||||||
|
}) => {
|
||||||
|
const payload = [];
|
||||||
|
payload.push("### Uptime Kuma\n");
|
||||||
|
payload.push(this._statusMessageFactory(status, monitorName));
|
||||||
|
payload.push(`*Description:* ${monitorMessage}`);
|
||||||
|
|
||||||
|
if (monitorName) {
|
||||||
|
payload.push(`*Monitor:* ${monitorName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitorUrl && monitorUrl !== "https://") {
|
||||||
|
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a general notification
|
||||||
|
* @param {string} webhookUrl URL to send request to
|
||||||
|
* @param {string} msg Message to send
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._sendNotification(webhookUrl, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
await this._handleGeneralNotification(notification.webhookUrl, msg);
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
switch (monitorJSON["type"]) {
|
||||||
|
case "http":
|
||||||
|
case "keywork":
|
||||||
|
url = monitorJSON["url"];
|
||||||
|
break;
|
||||||
|
case "docker":
|
||||||
|
url = monitorJSON["docker_host"];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
url = monitorJSON["hostname"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: heartbeatJSON.msg,
|
||||||
|
monitorName: monitorJSON.name,
|
||||||
|
monitorUrl: url,
|
||||||
|
status: heartbeatJSON.status
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._sendNotification(notification.webhookUrl, payload);
|
||||||
|
return okMsg;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ZohoCliq;
|
@ -1,199 +0,0 @@
|
|||||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
|
||||||
// Fixed on Windows
|
|
||||||
const net = require("net");
|
|
||||||
const spawn = require("child_process").spawn;
|
|
||||||
const events = require("events");
|
|
||||||
const fs = require("fs");
|
|
||||||
const util = require("./util-server");
|
|
||||||
|
|
||||||
module.exports = Ping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for ping class
|
|
||||||
* @param {string} host Host to ping
|
|
||||||
* @param {object} [options] Options for the ping command
|
|
||||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
|
||||||
*/
|
|
||||||
function Ping(host, options) {
|
|
||||||
if (!host) {
|
|
||||||
throw new Error("You must specify a host to ping!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._host = host;
|
|
||||||
this._options = options = (options || {});
|
|
||||||
|
|
||||||
events.EventEmitter.call(this);
|
|
||||||
|
|
||||||
const timeout = 10;
|
|
||||||
|
|
||||||
if (util.WIN) {
|
|
||||||
this._bin = "c:/windows/system32/ping.exe";
|
|
||||||
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
|
|
||||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
|
||||||
|
|
||||||
} else if (util.LIN) {
|
|
||||||
this._bin = "/bin/ping";
|
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
|
|
||||||
|
|
||||||
if (net.isIPv6(host) || options.ipv6) {
|
|
||||||
defaultArgs.unshift("-6");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._args = (options.args) ? options.args : defaultArgs;
|
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
|
||||||
|
|
||||||
} else if (util.MAC) {
|
|
||||||
|
|
||||||
if (net.isIPv6(host) || options.ipv6) {
|
|
||||||
this._bin = "/sbin/ping6";
|
|
||||||
} else {
|
|
||||||
this._bin = "/sbin/ping";
|
|
||||||
}
|
|
||||||
|
|
||||||
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
|
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
|
||||||
|
|
||||||
} else if (util.BSD) {
|
|
||||||
this._bin = "/sbin/ping";
|
|
||||||
|
|
||||||
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
|
|
||||||
|
|
||||||
if (net.isIPv6(host) || options.ipv6) {
|
|
||||||
defaultArgs.unshift("-6");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._args = (options.args) ? options.args : defaultArgs;
|
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new Error("Could not detect your ping binary.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(this._bin)) {
|
|
||||||
throw new Error("Could not detect " + this._bin + " on your system");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._i = 0;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback for send
|
|
||||||
* @callback pingCB
|
|
||||||
* @param {any} err Any error encountered
|
|
||||||
* @param {number} ms Ping time in ms
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a ping
|
|
||||||
* @param {pingCB} callback Callback to call with results
|
|
||||||
*/
|
|
||||||
Ping.prototype.send = function (callback) {
|
|
||||||
let self = this;
|
|
||||||
callback = callback || function (err, ms) {
|
|
||||||
if (err) {
|
|
||||||
return self.emit("error", err);
|
|
||||||
}
|
|
||||||
return self.emit("result", ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ended;
|
|
||||||
let _exited;
|
|
||||||
let _errored;
|
|
||||||
|
|
||||||
this._ping = spawn(this._bin, this._args); // spawn the binary
|
|
||||||
|
|
||||||
this._ping.on("error", function (err) { // handle binary errors
|
|
||||||
_errored = true;
|
|
||||||
callback(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ping.stdout.on("data", function (data) { // log stdout
|
|
||||||
if (util.WIN) {
|
|
||||||
data = convertOutput(data);
|
|
||||||
}
|
|
||||||
this._stdout = (this._stdout || "") + data;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ping.stdout.on("end", function () {
|
|
||||||
_ended = true;
|
|
||||||
if (_exited && !_errored) {
|
|
||||||
onEnd.call(self._ping);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ping.stderr.on("data", function (data) { // log stderr
|
|
||||||
if (util.WIN) {
|
|
||||||
data = convertOutput(data);
|
|
||||||
}
|
|
||||||
this._stderr = (this._stderr || "") + data;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._ping.on("exit", function (code) { // handle complete
|
|
||||||
_exited = true;
|
|
||||||
if (_ended && !_errored) {
|
|
||||||
onEnd.call(self._ping);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} callback
|
|
||||||
*
|
|
||||||
* Generated by Trelent
|
|
||||||
*/
|
|
||||||
function onEnd() {
|
|
||||||
let stdout = this.stdout._stdout;
|
|
||||||
let stderr = this.stderr._stderr;
|
|
||||||
let ms;
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
return callback(new Error(stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stdout) {
|
|
||||||
return callback(new Error("No stdout detected"));
|
|
||||||
}
|
|
||||||
|
|
||||||
ms = stdout.match(self._regmatch); // parse out the ##ms response
|
|
||||||
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
|
|
||||||
|
|
||||||
callback(null, ms, stdout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ping every interval
|
|
||||||
* @param {pingCB} callback Callback to call with results
|
|
||||||
*/
|
|
||||||
Ping.prototype.start = function (callback) {
|
|
||||||
let self = this;
|
|
||||||
this._i = setInterval(function () {
|
|
||||||
self.send(callback);
|
|
||||||
}, (self._options.interval || 5000));
|
|
||||||
self.send(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Stop sending pings */
|
|
||||||
Ping.prototype.stop = function () {
|
|
||||||
clearInterval(this._i);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
|
|
||||||
* Thank @pemassi
|
|
||||||
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
|
|
||||||
* @param {any} data
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function convertOutput(data) {
|
|
||||||
if (util.WIN) {
|
|
||||||
if (data) {
|
|
||||||
return util.convertToUTF8(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
class Plugin {
|
||||||
|
async load() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Plugin,
|
||||||
|
};
|
@ -0,0 +1,256 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
const path = require("path");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { Git } = require("./git");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
class PluginsManager {
|
||||||
|
|
||||||
|
static disable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin List
|
||||||
|
* @type {PluginWrapper[]}
|
||||||
|
*/
|
||||||
|
pluginList = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins Dir
|
||||||
|
*/
|
||||||
|
pluginsDir;
|
||||||
|
|
||||||
|
server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
*/
|
||||||
|
constructor(server) {
|
||||||
|
this.server = server;
|
||||||
|
|
||||||
|
if (!PluginsManager.disable) {
|
||||||
|
this.pluginsDir = "./data/plugins/";
|
||||||
|
|
||||||
|
if (! fs.existsSync(this.pluginsDir)) {
|
||||||
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("plugin", "Scanning plugin directory");
|
||||||
|
let list = fs.readdirSync(this.pluginsDir);
|
||||||
|
|
||||||
|
this.pluginList = [];
|
||||||
|
for (let item of list) {
|
||||||
|
this.loadPlugin(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.warn("PLUGIN", "Skip scanning plugin directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a Plugin
|
||||||
|
*/
|
||||||
|
async loadPlugin(name) {
|
||||||
|
log.info("plugin", "Load " + name);
|
||||||
|
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await plugin.load();
|
||||||
|
this.pluginList.push(plugin);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
|
||||||
|
log.error("plugin", "Reason: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a Plugin
|
||||||
|
* @param {string} repoURL Git repo url
|
||||||
|
* @param {string} name Directory name, also known as plugin unique name
|
||||||
|
*/
|
||||||
|
downloadPlugin(repoURL, name) {
|
||||||
|
if (fs.existsSync(this.pluginsDir + name)) {
|
||||||
|
log.info("plugin", "Plugin folder already exists? Removing...");
|
||||||
|
fs.rmSync(this.pluginsDir + name, {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
|
||||||
|
let result = Git.clone(repoURL, this.pluginsDir, name);
|
||||||
|
log.info("plugin", "Install result: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a plugin
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async removePlugin(name) {
|
||||||
|
log.info("plugin", "Removing plugin: " + name);
|
||||||
|
for (let plugin of this.pluginList) {
|
||||||
|
if (plugin.info.name === name) {
|
||||||
|
await plugin.unload();
|
||||||
|
|
||||||
|
// Delete the plugin directory
|
||||||
|
fs.rmSync(this.pluginsDir + name, {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("plugin", "Plugin not found: " + name);
|
||||||
|
throw new Error("Plugin not found: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Update a plugin
|
||||||
|
* Only available for plugins which were downloaded from the official list
|
||||||
|
* @param pluginID
|
||||||
|
*/
|
||||||
|
updatePlugin(pluginID) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin list from server + local installed plugin list
|
||||||
|
* Item will be merged if the `name` is the same.
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async fetchPluginList() {
|
||||||
|
let remotePluginList;
|
||||||
|
try {
|
||||||
|
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
|
||||||
|
remotePluginList = res.data.pluginList;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("plugin", "Failed to fetch plugin list: " + e.message);
|
||||||
|
remotePluginList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let plugin of this.pluginList) {
|
||||||
|
let find = false;
|
||||||
|
// Try to merge
|
||||||
|
for (let remotePlugin of remotePluginList) {
|
||||||
|
if (remotePlugin.name === plugin.info.name) {
|
||||||
|
find = true;
|
||||||
|
remotePlugin.installed = true;
|
||||||
|
remotePlugin.name = plugin.info.name;
|
||||||
|
remotePlugin.fullName = plugin.info.fullName;
|
||||||
|
remotePlugin.description = plugin.info.description;
|
||||||
|
remotePlugin.version = plugin.info.version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local plugin
|
||||||
|
if (!find) {
|
||||||
|
plugin.info.local = true;
|
||||||
|
remotePluginList.push(plugin.info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort Installed first, then sort by name
|
||||||
|
return remotePluginList.sort((a, b) => {
|
||||||
|
if (a.installed === b.installed) {
|
||||||
|
if (a.fullName < b.fullName) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.fullName > b.fullName) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else if (a.installed) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginWrapper {
|
||||||
|
|
||||||
|
server = undefined;
|
||||||
|
pluginDir = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be an `new-able` class.
|
||||||
|
* @type {function}
|
||||||
|
*/
|
||||||
|
pluginClass = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Plugin}
|
||||||
|
*/
|
||||||
|
object = undefined;
|
||||||
|
info = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
* @param {string} pluginDir
|
||||||
|
*/
|
||||||
|
constructor(server, pluginDir) {
|
||||||
|
this.server = server;
|
||||||
|
this.pluginDir = pluginDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let indexFile = this.pluginDir + "/index.js";
|
||||||
|
let packageJSON = this.pluginDir + "/package.json";
|
||||||
|
|
||||||
|
log.info("plugin", "Installing dependencies");
|
||||||
|
|
||||||
|
if (fs.existsSync(indexFile)) {
|
||||||
|
// Install dependencies
|
||||||
|
let result = childProcess.spawnSync("npm", [ "install" ], {
|
||||||
|
cwd: this.pluginDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.stdout) {
|
||||||
|
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
|
||||||
|
} else {
|
||||||
|
log.warn("plugin", "Install dependencies result: no output");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pluginClass = require(path.join(process.cwd(), indexFile));
|
||||||
|
|
||||||
|
let pluginClassType = typeof this.pluginClass;
|
||||||
|
|
||||||
|
if (pluginClassType === "function") {
|
||||||
|
this.object = new this.pluginClass(this.server);
|
||||||
|
await this.object.load();
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid plugin, it does not export a class");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(packageJSON)) {
|
||||||
|
this.info = require(path.join(process.cwd(), packageJSON));
|
||||||
|
} else {
|
||||||
|
this.info.fullName = this.pluginDir;
|
||||||
|
this.info.name = "[unknown]";
|
||||||
|
this.info.version = "[unknown-version]";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.info.installed = true;
|
||||||
|
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
await this.object.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PluginsManager,
|
||||||
|
PluginWrapper
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
const { log } = require("../../src/util");
|
||||||
|
const { Settings } = require("../settings");
|
||||||
|
const { sendInfo } = require("../client");
|
||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const GameResolver = require("gamedig/lib/GameResolver");
|
||||||
|
|
||||||
|
let gameResolver = new GameResolver();
|
||||||
|
let gameList = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a game list via GameDig
|
||||||
|
* @returns {any[]}
|
||||||
|
*/
|
||||||
|
function getGameList() {
|
||||||
|
if (!gameList) {
|
||||||
|
gameList = gameResolver._readGames().games.sort((a, b) => {
|
||||||
|
if ( a.pretty < b.pretty ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if ( a.pretty > b.pretty ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return gameList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.generalSocketHandler = (socket, server) => {
|
||||||
|
|
||||||
|
socket.on("initServerTimezone", async (timezone) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
log.debug("generalSocketHandler", "Timezone: " + timezone);
|
||||||
|
await Settings.set("initServerTimezone", true);
|
||||||
|
await server.setTimezone(timezone);
|
||||||
|
await sendInfo(socket);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("initServerTimezone", e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getGameList", async (callback) => {
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
gameList: getGameList(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
@ -0,0 +1,317 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
|
const Maintenance = require("../model/maintenance");
|
||||||
|
const server = UptimeKumaServer.getInstance();
|
||||||
|
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for Maintenance
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
*/
|
||||||
|
module.exports.maintenanceSocketHandler = (socket) => {
|
||||||
|
// Add a new maintenance
|
||||||
|
socket.on("addMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", maintenance);
|
||||||
|
|
||||||
|
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||||
|
bean.user_id = socket.userID;
|
||||||
|
let maintenanceID = await R.store(bean);
|
||||||
|
await MaintenanceTimeslot.generateTimeslot(bean);
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
maintenanceID,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit a maintenance
|
||||||
|
socket.on("editMaintenance", async (maintenance, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
||||||
|
|
||||||
|
if (bean.user_id !== socket.userID) {
|
||||||
|
throw new Error("Permission denied.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Maintenance.jsonToBean(bean, maintenance);
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved.",
|
||||||
|
maintenanceID: bean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new monitor_maintenance
|
||||||
|
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
|
||||||
|
maintenanceID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for await (const monitor of monitors) {
|
||||||
|
let bean = R.dispense("monitor_maintenance");
|
||||||
|
|
||||||
|
bean.import({
|
||||||
|
monitor_id: monitor.id,
|
||||||
|
maintenance_id: maintenanceID
|
||||||
|
});
|
||||||
|
await R.store(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a new monitor_maintenance
|
||||||
|
socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
|
||||||
|
maintenanceID
|
||||||
|
]);
|
||||||
|
|
||||||
|
for await (const statusPage of statusPages) {
|
||||||
|
let bean = R.dispense("maintenance_status_page");
|
||||||
|
|
||||||
|
bean.import({
|
||||||
|
status_page_id: statusPage.id,
|
||||||
|
maintenance_id: maintenanceID
|
||||||
|
});
|
||||||
|
await R.store(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Added Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
maintenance: await bean.toJSON(),
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenanceList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
monitors,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
statusPages,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
if (maintenanceID in server.maintenanceList) {
|
||||||
|
delete server.maintenanceList[maintenanceID];
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Deleted Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("pauseMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Paused Successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("resumeMaintenance", async (maintenanceID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
|
||||||
|
maintenanceID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Resume Successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.sendMaintenanceList(socket);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,69 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { PluginsManager } = require("../plugins-manager");
|
||||||
|
const { log } = require("../../src/util.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for plugins
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
*/
|
||||||
|
module.exports.pluginsHandler = (socket, server) => {
|
||||||
|
|
||||||
|
const pluginManager = server.getPluginManager();
|
||||||
|
|
||||||
|
// Get Plugin List
|
||||||
|
socket.on("getPluginList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
|
||||||
|
|
||||||
|
if (PluginsManager.disable) {
|
||||||
|
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pluginList = await pluginManager.fetchPluginList();
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
pluginList,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.warn("plugin", "Error: " + error.message);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("installPlugin", async (repoURL, name, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
pluginManager.downloadPlugin(repoURL, name);
|
||||||
|
await pluginManager.loadPlugin(name);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("uninstallPlugin", async (name, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await pluginManager.removePlugin(name);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,49 @@
|
|||||||
|
const { log } = require("../src/util");
|
||||||
|
class UptimeCacheList {
|
||||||
|
/**
|
||||||
|
* list[monitorID][duration]
|
||||||
|
*/
|
||||||
|
static list = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the uptime for a specific period
|
||||||
|
* @param {number} monitorID
|
||||||
|
* @param {number} duration
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
static getUptime(monitorID, duration) {
|
||||||
|
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
|
||||||
|
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
|
||||||
|
return UptimeCacheList.list[monitorID][duration];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add uptime for specified monitor
|
||||||
|
* @param {number} monitorID
|
||||||
|
* @param {number} duration
|
||||||
|
* @param {number} uptime Uptime to add
|
||||||
|
*/
|
||||||
|
static addUptime(monitorID, duration, uptime) {
|
||||||
|
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
|
||||||
|
if (!UptimeCacheList.list[monitorID]) {
|
||||||
|
UptimeCacheList.list[monitorID] = {};
|
||||||
|
}
|
||||||
|
UptimeCacheList.list[monitorID][duration] = uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache for specified monitor
|
||||||
|
* @param {number} monitorID
|
||||||
|
*/
|
||||||
|
static clearCache(monitorID) {
|
||||||
|
log.debug("UptimeCacheList", "clearCache: " + monitorID);
|
||||||
|
delete UptimeCacheList.list[monitorID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
UptimeCacheList,
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
@import "@vuepic/vue-datepicker/dist/main.css";
|
||||||
|
@import "vars.scss";
|
||||||
|
|
||||||
|
// Must use #{ }
|
||||||
|
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
|
||||||
|
.dp__theme_dark {
|
||||||
|
--dp-background-color: #{$dark-bg2};
|
||||||
|
--dp-text-color: #{$dark-font-color};
|
||||||
|
--dp-hover-color: #484848;
|
||||||
|
--dp-hover-text-color: #ffffff;
|
||||||
|
--dp-hover-icon-color: #959595;
|
||||||
|
--dp-primary-color: #{#5cdd8b};
|
||||||
|
--dp-primary-text-color: #ffffff;
|
||||||
|
--dp-secondary-color: #494949;
|
||||||
|
--dp-border-color: #{$dark-border-color};
|
||||||
|
--dp-menu-border-color: #2d2d2d;
|
||||||
|
--dp-border-color-hover: #{$dark-border-color};
|
||||||
|
--dp-disabled-color: #212121;
|
||||||
|
--dp-scroll-bar-background: #212121;
|
||||||
|
--dp-scroll-bar-color: #484848;
|
||||||
|
--dp-success-color: #{$primary};
|
||||||
|
--dp-success-color-disabled: #428f59;
|
||||||
|
--dp-icon-color: #959595;
|
||||||
|
--dp-danger-color: #e53935;
|
||||||
|
--dp-highlight-color: rgba(0, 92, 178, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__input {
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
|
||||||
|
.dp__main > div[aria-label="Datepicker input"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue