Merge branch 'master' into clean-mobile-table

This commit is contained in:
LouisLam 2021-08-24 01:53:13 +08:00
commit 3f12afce28
11 changed files with 412 additions and 10479 deletions

View file

@ -2,10 +2,10 @@
FROM node:14-alpine3.12 AS release FROM node:14-alpine3.12 AS release
WORKDIR /app WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt # split the sqlite install here, so that it can caches the prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
ln -s /usr/bin/python3 /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python && \
npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \ npm install better-sqlite3@7.4.3 bcrypt@5.0.1 && \
apk del .build-deps && \ apk del .build-deps && \
rm -f /usr/bin/python rm -f /usr/bin/python

10724
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.3.1", "version": "1.3.2",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -17,10 +17,10 @@
"update": "", "update": "",
"build": "vite build", "build": "vite build",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.3.1 --target release . --push", "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.3.2 --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.3.1 && npm install && npm run build", "setup": "git checkout 1.3.2 && npm install && npm run build",
"update-version": "node extra/update-version.js", "update-version": "node extra/update-version.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",
@ -36,11 +36,11 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4", "@fortawesome/vue-fontawesome": "^3.0.0-4",
"@louislam/sqlite3": "^5.0.3",
"@popperjs/core": "^2.9.3", "@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0", "args-parser": "^1.3.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"better-sqlite3": "^7.4.3",
"bootstrap": "^5.1.0", "bootstrap": "^5.1.0",
"chart.js": "^3.5.0", "chart.js": "^3.5.0",
"chartjs-adapter-dayjs": "^1.0.0", "chartjs-adapter-dayjs": "^1.0.0",
@ -56,7 +56,7 @@
"password-hash": "^1.2.2", "password-hash": "^1.2.2",
"prom-client": "^13.2.0", "prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0", "prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.21", "redbean-node": "0.1.1",
"socket.io": "^4.1.3", "socket.io": "^4.1.3",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",

View file

@ -28,7 +28,7 @@ exports.startInterval = () => {
}; };
check(); check();
interval = setInterval(check, 3600 * 48); interval = setInterval(check, 3600 * 1000 * 48);
}; };
exports.enableCheckUpdate = async (value) => { exports.enableCheckUpdate = async (value) => {

View file

@ -3,7 +3,6 @@ const { sleep, debug, isDev } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const knex = require("knex"); const knex = require("knex");
const sqlite3 = require("@louislam/sqlite3");
class Database { class Database {
@ -14,48 +13,23 @@ class Database {
static sqliteInstance = null; static sqliteInstance = null;
static async connect() { static async connect() {
const acquireConnectionTimeout = 120 * 1000;
if (! this.sqliteInstance) { R.useBetterSQLite3 = true;
this.sqliteInstance = new sqlite3.Database(Database.path); R.betterSQLite3Options.timeout = acquireConnectionTimeout;
this.sqliteInstance.run("PRAGMA journal_mode = WAL");
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js"); R.setup("sqlite", {
Dialect.prototype._driver = () => sqlite3; filename: Database.path,
if (isDev) {
Dialect.prototype.acquireConnectionOrg = Dialect.prototype.acquireConnection;
Dialect.prototype.acquireConnection = async function () {
let a = await this.acquireConnectionOrg();
debug("acquired Connection");
return a;
};
}
// Always return same connection.
Dialect.prototype.acquireRawConnection = async function () {
return Database.sqliteInstance;
};
Dialect.prototype.destroyRawConnection = async () => { }
const knexInstance = knex({
client: Dialect,
connection: { }, // Do not remove, Leave it empty is ok
useNullAsDefault: true, useNullAsDefault: true,
pool: { acquireConnectionTimeout: acquireConnectionTimeout,
min: 1, }, {
max: 5, min: 1,
idleTimeoutMillis: 30000, max: 1,
} idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
}); });
console.log( knexInstance.pool)
console.log("pool size")
R.setup(knexInstance);
if (process.env.SQL_LOG === "1") { if (process.env.SQL_LOG === "1") {
R.debug(true); R.debug(true);
} }
@ -63,6 +37,10 @@ class Database {
// Auto map the model to a bean object // Auto map the model to a bean object
R.freeze(true) R.freeze(true)
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
} }
static async patch() { static async patch() {

View file

@ -110,10 +110,9 @@ class Monitor extends BeanModel {
try { try {
if (this.type === "http" || this.type === "keyword") { if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
// Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, { let res = await axios.get(this.url, {
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
@ -121,7 +120,7 @@ class Monitor extends BeanModel {
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: ! this.getIgnoreTls(),
}), }),
maxRedirects: this.maxredirects, maxRedirects: this.maxredirects,
@ -241,7 +240,8 @@ class Monitor extends BeanModel {
try { try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) { } catch (e) {
console.error("Cannot send notification to " + notification.name) console.error("Cannot send notification to " + notification.name);
console.log(e);
} }
} }
} }
@ -258,22 +258,22 @@ class Monitor extends BeanModel {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
} }
prometheus.update(bean, tlsInfo)
io.to(this.user_id).emit("heartbeat", bean.toJSON()); io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)
Monitor.sendStats(io, this.id, this.user_id) Monitor.sendStats(io, this.id, this.user_id)
await R.store(bean);
prometheus.update(bean, tlsInfo);
previousBeat = bean; previousBeat = bean;
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
} }
beat(); beat();
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
} }
stop() { stop() {
clearInterval(this.heartbeatInterval) clearTimeout(this.heartbeatInterval);
} }
/** /**

View file

@ -150,7 +150,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user) { if (user) {
debug("afterLogin") debug("afterLogin")
await afterLogin(socket, user) afterLogin(socket, user)
debug("afterLogin ok") debug("afterLogin ok")
@ -178,7 +178,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let user = await login(data.username, data.password) let user = await login(data.username, data.password)
if (user) { if (user) {
await afterLogin(socket, user) afterLogin(socket, user)
callback({ callback({
ok: true, ok: true,
@ -561,7 +561,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
debug("check auto login") debug("check auto login")
if (await setting("disableAuth")) { if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin") console.log("Disabled Auth: auto login to admin")
await afterLogin(socket, await R.findOne("user")) afterLogin(socket, await R.findOne("user"))
socket.emit("autoLogin") socket.emit("autoLogin")
} else { } else {
debug("need auth") debug("need auth")
@ -621,6 +621,8 @@ async function sendMonitorList(socket) {
} }
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = []; let result = [];
let list = await R.find("notification", " user_id = ? ", [ let list = await R.find("notification", " user_id = ? ", [
socket.userID, socket.userID,
@ -631,6 +633,9 @@ async function sendNotificationList(socket) {
} }
io.to(socket.userID).emit("notificationList", result) io.to(socket.userID).emit("notificationList", result)
timeLogger.print("Send Notification List");
return list; return list;
} }
@ -639,24 +644,27 @@ async function afterLogin(socket, user) {
socket.join(user.id) socket.join(user.id)
let monitorList = await sendMonitorList(socket) let monitorList = await sendMonitorList(socket)
sendNotificationList(socket) sendNotificationList(socket)
// Delay a bit, so that it let the main page to query the data first, since SQLite can process one sql at the same time only. await sleep(500);
// For example, query the edit data first.
setTimeout(async () => { for (let monitorID in monitorList) {
for (let monitorID in monitorList) { await sendHeartbeatList(socket, monitorID);
sendHeartbeatList(socket, monitorID); }
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id) for (let monitorID in monitorList) {
} await sendImportantHeartbeatList(socket, monitorID);
}, 500); }
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id)
}
} }
async function getMonitorJSONList(userID) { async function getMonitorJSONList(userID) {
let result = {}; let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [ let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID, userID,
]) ])
@ -786,6 +794,8 @@ async function sendHeartbeatList(socket, monitorID) {
} }
socket.emit("heartbeatList", monitorID, result) socket.emit("heartbeatList", monitorID, result)
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`)
} }
async function sendImportantHeartbeatList(socket, monitorID) { async function sendImportantHeartbeatList(socket, monitorID) {

View file

@ -1,6 +1,6 @@
<template> <template>
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div> <div v-if="monitor">
<h1> {{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<p class="url"> <p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a> <a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>

View file

@ -1,5 +1,7 @@
<template> <template>
<MonitorList /> <transition name="slide-fade" appear>
<MonitorList />
</transition>
</template> </template>
<script> <script>

View file

@ -1,4 +1,3 @@
// @ts-nocheck
// Common Util for frontend and backend // Common Util for frontend and backend
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
@ -13,7 +12,7 @@ export const DOWN = 0;
export const UP = 1; export const UP = 1;
export const PENDING = 2; export const PENDING = 2;
export function flipStatus(s) { export function flipStatus(s: number) {
if (s === UP) { if (s === UP) {
return DOWN; return DOWN;
} }
@ -25,7 +24,7 @@ export function flipStatus(s) {
return s; return s;
} }
export function sleep(ms) { export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
@ -33,8 +32,8 @@ export function sleep(ms) {
* PHP's ucfirst * PHP's ucfirst
* @param str * @param str
*/ */
export function ucfirst(str) { export function ucfirst(str: string) {
if (! str) { if (!str) {
return str; return str;
} }
@ -42,12 +41,15 @@ export function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1); return firstLetter.toUpperCase() + str.substr(1);
} }
export function debug(msg) { export function debug(msg: any) {
if (isDev) { if (isDev) {
console.log(msg); console.log(msg);
} }
} }
declare global { interface String { replaceAll(str: string, newStr: string): string; } }
export function polyfill() { export function polyfill() {
/** /**
* String.prototype.replaceAll() polyfill * String.prototype.replaceAll() polyfill
@ -56,7 +58,7 @@ export function polyfill() {
* @license MIT * @license MIT
*/ */
if (!String.prototype.replaceAll) { if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) { String.prototype.replaceAll = function (str: string, newStr: string) {
// If a regex pattern // If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
@ -71,11 +73,13 @@ export function polyfill() {
} }
export class TimeLogger { export class TimeLogger {
startTime: number;
constructor() { constructor() {
this.startTime = dayjs().valueOf(); this.startTime = dayjs().valueOf();
} }
print(name) { print(name: string) {
if (isDev) { if (isDev) {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms") console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
} }
@ -85,7 +89,7 @@ export class TimeLogger {
/** /**
* Returns a random number between min (inclusive) and max (exclusive) * Returns a random number between min (inclusive) and max (exclusive)
*/ */
export function getRandomArbitrary(min, max) { export function getRandomArbitrary(min: number, max: number) {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
} }
@ -98,7 +102,7 @@ export function getRandomArbitrary(min, max) {
* lower than max if max isn't an integer). * lower than max if max isn't an integer).
* Using Math.round() will give you a non-uniform distribution! * Using Math.round() will give you a non-uniform distribution!
*/ */
export function getRandomInt(min, max) { export function getRandomInt(min: number, max: number) {
min = Math.ceil(min); min = Math.ceil(min);
max = Math.floor(max); max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;

View file

@ -4,14 +4,15 @@
"target": "es2018", "target": "es2018",
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": [
"es2020" "es2020",
"DOM",
], ],
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"sourceMap": false, "sourceMap": false,
"files.insertFinalNewline": true "strict": true
}, },
"files": [ "files": [
"./server/util.ts" "./src/util.ts"
] ]
} }