Merge pull request #1 from louislam/master

Pull down upstream
pull/101/head
Matthew Macdonald-Wallace 3 years ago committed by GitHub
commit ef41a32353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,10 @@
---
name: ⚠ Please go to "Discussions" Tab if you want to ask or share something
about: BUG REPORT ONLY HERE
title: ''
labels: ''
assignees: ''
---
BUG REPORT ONLY HERE

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Additional context**
Add any other context about the problem here.

@ -27,7 +27,7 @@ It is a self-hosted monitoring tool like "Uptime Robot".
docker volume create uptime-kuma
# Start the container
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
Browse to http://localhost:3001 after started.
@ -35,7 +35,7 @@ Browse to http://localhost:3001 after started.
Change Port and Volume
```bash
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
### Without Docker
@ -80,12 +80,17 @@ PS: For every new release, it takes some time to build the docker image, please
```bash
git fetch --all
git checkout 1.0.5 --force
git checkout 1.0.6 --force
npm install
npm run build
pm2 restart uptime-kuma
```
# What's Next?
I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
# More Screenshots
Settings Page:
@ -112,7 +117,7 @@ If you love this project, please consider giving me a ⭐.
# Contribute
If you want to report a bug or request a new featue. Free feel to open a new issue.
If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/wiki/%5BDev%5D-Setup-Development-Environment

@ -0,0 +1,37 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
-- SQL Generated by Intellij Idea
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
create table monitor_dg_tmp
(
id INTEGER not null
primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER
references user
on update cascade on delete set null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME,
keyword VARCHAR(255)
);
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
drop table monitor;
alter table monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys=on;

22
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.0.5",
"version": "1.0.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -392,6 +392,11 @@
"follow-redirects": "^1.10.0"
}
},
"babel-plugin-add-module-exports": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz",
"integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU="
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@ -2093,6 +2098,11 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz",
"integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@ -3698,6 +3708,16 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"optional": true
},
"v-pagination-3": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/v-pagination-3/-/v-pagination-3-0.1.6.tgz",
"integrity": "sha512-82J8HnEIYtZijn6F3xhyP/ildI5K7Rv4Yu74VNfQWQsiPWTKntgVvZgBH8UPh/lFEjgWxty/M4N+YHvS+YbGzg==",
"requires": {
"babel-plugin-add-module-exports": "^0.2.1",
"merge": "^2.1.1",
"vue": ">=3.0.0"
}
},
"v8flags": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.0.5",
"version": "1.0.6",
"license": "MIT",
"repository": {
"type": "git",
@ -12,10 +12,10 @@
"update": "",
"build": "vite build",
"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.0.5 --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.0.6 --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-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.5 && npm install && npm run build",
"setup": "git checkout 1.0.6 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js",
"mark-as-nightly": "node extra/mark-as-nightly.js"
},
@ -38,6 +38,7 @@
"socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2",
"tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6",
"vue": "^3.0.5",
"vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10",

@ -0,0 +1,119 @@
const fs = require("fs");
const {sleep} = require("./util");
const {R} = require("redbean-node");
const {setSetting, setting} = require("./util-server");
class Database {
static templatePath = "./db/kuma.db"
static path = './data/kuma.db';
static latestVersion = 1;
static noReject = true;
static async patch() {
let version = parseInt(await setting("database_version"));
if (! version) {
version = 0;
}
console.info("Your database version: " + version);
console.info("Latest database version: " + this.latestVersion);
if (version === this.latestVersion) {
console.info("Database no need to patch");
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = "./data/kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
// Try catch anything here, if gone wrong, restore the backup
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`;
console.info(`Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile);
console.info(`Patched ${sqlFile}`);
await setSetting("database_version", i);
}
console.log("Database Patched Successfully");
} catch (ex) {
await Database.close();
console.error("Patch db failed!!! Restoring the backup")
fs.copyFileSync(backupPath, Database.path);
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed")
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
process.exit(1);
}
}
}
/**
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
* @param filename
* @returns {Promise<void>}
*/
static async importSQLFile(filename) {
await R.getCell("SELECT 1");
let text = fs.readFileSync(filename).toString();
// Remove all comments (--)
let lines = text.split("\n");
lines = lines.filter((line) => {
return ! line.startsWith("--")
});
// Split statements by semicolon
// Filter out empty line
text = lines.join("\n")
let statements = text.split(";")
.map((statement) => {
return statement.trim();
})
.filter((statement) => {
return statement !== "";
})
for (let statement of statements) {
await R.exec(statement);
}
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught
* @returns {Promise<void>}
*/
static async close() {
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener('unhandledRejection', listener);
console.log("Closing DB")
while (true) {
Database.noReject = true;
await R.close()
await sleep(2000)
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db")
}
}
console.log("SQLite closed")
process.removeListener('unhandledRejection', listener);
}
}
module.exports = Database;

@ -3,8 +3,6 @@ const utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const {R} = require("redbean-node");
const {BeanModel} = require("redbean-node/dist/bean-model");

@ -48,8 +48,6 @@ class Monitor extends BeanModel {
let previousBeat = null;
const beat = async () => {
console.log(`Monitor ${this.id}: Heartbeat`)
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id
@ -123,8 +121,6 @@ class Monitor extends BeanModel {
this.id
])
let promiseList = [];
let text;
if (bean.status === 1) {
text = "✅ Up"
@ -135,16 +131,24 @@ class Monitor extends BeanModel {
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for(let notification of notificationList) {
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
}
}
await Promise.all(promiseList);
}
} else {
bean.important = false;
}
if (bean.status === 1) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
} else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
}
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)

@ -72,7 +72,7 @@ class Notification {
finalData = data;
}
let res = await axios.post(notification.webhookURL, finalData, config)
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
@ -90,7 +90,7 @@ class Notification {
username: 'Uptime-Kuma',
content: msg
}
let res = await axios.post(notification.discordWebhookUrl, data)
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
@ -116,7 +116,7 @@ class Notification {
]
}]
}
let res = await axios.post(notification.discordWebhookUrl, data)
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch(error) {
throwGeneralAxiosError(error)
@ -131,7 +131,7 @@ class Notification {
};
let config = {};
let res = await axios.post(notification.signalURL, data, config)
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@ -141,7 +141,7 @@ class Notification {
try {
if (heartbeatJSON == null) {
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo}
let res = await axios.post(notification.slackwebhookURL, data)
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
@ -186,7 +186,7 @@ class Notification {
}
]
}
let res = await axios.post(notification.slackwebhookURL, data)
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@ -199,7 +199,7 @@ class Notification {
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>",
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds,
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1}
let res = await axios.post(pushoverlink, data)
await axios.post(pushoverlink, data)
return okMsg;
}
@ -214,7 +214,7 @@ class Notification {
"expire": "3600",
"html": 1
}
let res = await axios.post(pushoverlink, data)
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@ -278,7 +278,7 @@ class Notification {
});
// send mail with defined transport object
let info = await transporter.sendMail({
await transporter.sendMail({
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo,
subject: msg,

@ -12,6 +12,7 @@ const fs = require("fs");
const {getSettings} = require("./util-server");
const {Notification} = require("./notification")
const gracefulShutdown = require('http-graceful-shutdown');
const Database = require("./database");
const {sleep} = require("./util");
const args = require('args-parser')(process.argv);
@ -27,9 +28,28 @@ const server = http.createServer(app);
const io = new Server(server);
app.use(express.json())
/**
* Total WebSocket client connected to server currently, no actual use
* @type {number}
*/
let totalClient = 0;
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Main monitor list
* @type {{}}
*/
let monitorList = {};
/**
* Show Setup Page
* @type {boolean}
*/
let needSetup = false;
(async () => {
@ -50,7 +70,6 @@ let needSetup = false;
version,
})
console.log('a user connected');
totalClient++;
if (needSetup) {
@ -59,7 +78,6 @@ let needSetup = false;
}
socket.on('disconnect', () => {
console.log('user disconnected');
totalClient--;
});
@ -165,10 +183,6 @@ let needSetup = false;
msg: e.message
});
}
});
// Auth Only API
@ -557,19 +571,21 @@ function checkLogin(socket) {
}
async function initDatabase() {
const path = './data/kuma.db';
if (! fs.existsSync(path)) {
if (! fs.existsSync(Database.path)) {
console.log("Copying Database")
fs.copyFileSync("./db/kuma.db", path);
fs.copyFileSync(Database.templatePath, Database.path);
}
console.log("Connecting to Database")
R.setup('sqlite', {
filename: path
filename: Database.path
});
console.log("Connected")
// Patch the database
await Database.patch()
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
@ -589,6 +605,7 @@ async function initDatabase() {
console.log("Load JWT secret from database.")
}
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
if ((await R.count("user")) === 0) {
console.log("No user, need setup")
needSetup = true;
@ -707,11 +724,6 @@ const startGracefulShutdown = async () => {
}
let noReject = true;
process.on('unhandledRejection', (reason, p) => {
noReject = false;
});
async function shutdownFunction(signal) {
console.log('Called signal: ' + signal);
@ -720,24 +732,8 @@ async function shutdownFunction(signal) {
let monitor = monitorList[id]
monitor.stop()
}
await sleep(2000)
console.log("Closing DB")
// Special handle, because tarn.js throw a promise reject that cannot be caught
while (true) {
noReject = true;
await R.close()
await sleep(2000)
if (noReject) {
break;
} else {
console.log("Waiting...")
}
}
console.log("OK")
await sleep(2000);
await Database.close();
}
function finalFunction() {

@ -45,6 +45,18 @@ exports.setting = async function (key) {
])
}
exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [
key
])
if (! bean) {
bean = R.dispense("setting")
bean.key = key;
}
bean.value = value;
await R.store(bean)
}
exports.getSettings = async function (type) {
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
type

@ -55,7 +55,7 @@
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a>
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>

@ -35,7 +35,8 @@ export default {
window.addEventListener('resize', this.onResize);
let wsHost;
if (localStorage.dev === "dev") {
const env = process.env.NODE_ENV || "production";
if (env === "development" || localStorage.dev === "dev") {
wsHost = ":3001"
} else {
wsHost = ""
@ -45,6 +46,10 @@ export default {
transports: ['websocket']
});
socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
});
socket.on('info', (info) => {
this.info = info;
});

@ -13,7 +13,7 @@
No Monitors, please <router-link to="/add">add one</router-link>.
</div>
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in sortedMonitorList" @click="$root.cancelActiveList">
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index">
<div class="row">
<div class="col-6 col-md-8 small-padding">

@ -47,7 +47,7 @@
</tr>
</thead>
<tbody>
<tr v-for="beat in importantHeartBeatList">
<tr v-for="(beat, index) in displayedRecords" :key="index">
<td>{{ beat.name }}</td>
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
@ -59,6 +59,13 @@
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records=importantHeartBeatList.length
:per-page="perPage" />
</div>
</div>
</div>
@ -68,8 +75,21 @@
<script>
import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue";
import Pagination from "v-pagination-3";
export default {
components: {Datetime, Status},
components: {
Datetime,
Status,
Pagination,
},
data() {
return {
page: 1,
perPage: 25,
heartBeatList: [],
}
},
computed: {
stats() {
let result = {
@ -127,8 +147,16 @@ export default {
}
});
this.heartBeatList = result;
return result;
}
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
}
}
</script>

@ -64,7 +64,7 @@
</tr>
</thead>
<tbody>
<tr v-for="beat in importantHeartBeatList">
<tr v-for="(beat, index) in displayedRecords" :key="index">
<td><Status :status="beat.status" /></td>
<td><Datetime :value="beat.time" /></td>
<td>{{ beat.msg }}</td>
@ -75,6 +75,13 @@
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records=importantHeartBeatList.length
:per-page="perPage" />
</div>
</div>
<Confirm ref="confirmPause" @yes="pauseMonitor">
@ -95,6 +102,7 @@ import Status from "../components/Status.vue";
import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3";
export default {
components: {
@ -104,13 +112,16 @@ export default {
HeartbeatBar,
Confirm,
Status,
Pagination,
},
mounted() {
},
data() {
return {
page: 1,
perPage: 25,
heartBeatList: [],
}
},
computed: {
@ -154,6 +165,7 @@ export default {
importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) {
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id]
} else {
return [];
@ -166,8 +178,13 @@ export default {
} else {
return { }
}
}
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
methods: {
testNotification() {

@ -61,7 +61,7 @@
<h2>Notifications</h2>
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
<div class="form-check form-switch mb-3" v-for="notification in $root.notificationList">
<div class="form-check form-switch mb-3" v-for="(notification, index) in $root.notificationList" :key="index">
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
<label class="form-check-label" :for=" 'notification' + notification.id">

@ -11,7 +11,7 @@
<label for="timezone" class="form-label">Timezone</label>
<select class="form-select" id="timezone" v-model="$root.userTimezone">
<option value="auto">Auto: {{ guessTimezone }}</option>
<option v-for="timezone in timezoneList" :value="timezone.value">{{ timezone.name }}</option>
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option>
</select>
</div>
@ -59,7 +59,7 @@
<p v-else>Please assign the notification to monitor(s) to get it works.</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li class="list-group-item" v-for="notification in $root.notificationList">
<li class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index">
{{ notification.name }}<br />
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
</li>

@ -29,6 +29,7 @@ function getTimezoneOffset(timeZone) {
}
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
// TODO: Move to separate file
const aryIannaTimeZones = [
'Europe/Andorra',
'Asia/Dubai',
@ -412,4 +413,3 @@ export function timezoneList() {
return result;
};

Loading…
Cancel
Save