many update

pull/6/head
Louis 2021-06-29 16:06:20 +08:00
parent 932ebdb8d6
commit 9fa84a0a2b
10 changed files with 235 additions and 24 deletions

31
server/model/heartbeat.js Normal file
View File

@ -0,0 +1,31 @@
const dayjs = require("dayjs");
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");
/**
* status:
* 0 = DOWN
* 1 = UP
*/
class Heartbeat extends BeanModel {
toJSON() {
return {
monitorID: this.monitor_id,
status: this.status,
time: this.time,
msg: this.msg,
ping: this.ping,
important: this.important,
};
}
}
module.exports = Heartbeat;

View File

@ -28,9 +28,17 @@ class Monitor extends BeanModel {
}
start(io) {
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
])
}
let bean = R.dispense("heartbeat")
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
@ -49,15 +57,18 @@ class Monitor extends BeanModel {
bean.msg = error.message;
}
io.to(this.user_id).emit("heartbeat", {
monitorID: this.id,
status: bean.status,
time: bean.time,
msg: bean.msg,
ping: bean.ping,
});
// Mark as important if status changed
if (! previousBeat || previousBeat.status !== bean.status) {
bean.important = true;
} else {
bean.important = false;
}
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)
previousBeat = bean;
}
beat();

View File

@ -9,7 +9,6 @@ const {R} = require("redbean-node");
const passwordHash = require('password-hash');
const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor");
const {sleep} = require("./util");
let stop = false;
let interval = 6000;
@ -40,6 +39,10 @@ let monitorList = {};
// Public API
/*
firstConnect - true = send monitor list + heartbeat list history
false = do not send
*/
socket.on("loginByToken", async (token, callback) => {
try {
@ -320,13 +323,20 @@ async function checkOwner(userID, monitorID) {
}
async function sendMonitorList(socket) {
io.to(socket.userID).emit("monitorList", await getMonitorJSONList(socket.userID))
let list = await getMonitorJSONList(socket.userID);
io.to(socket.userID).emit("monitorList", list)
return list;
}
async function afterLogin(socket, user) {
socket.userID = user.id;
socket.join(user.id)
socket.emit("monitorList", await getMonitorJSONList(user.id))
let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
}
}
async function getMonitorJSONList(userID) {
@ -338,8 +348,11 @@ async function getMonitorJSONList(userID) {
for (let monitor of monitorList) {
result[monitor.id] = monitor.toJSON();
}
return result;
}
@ -421,3 +434,24 @@ async function startMonitors() {
}
}
/**
* Send Heartbeat History list to socket
*/
async function sendHeartbeatList(socket, monitorID) {
let list = await R.find("heartbeat", `
monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID
])
let result = [];
for (let bean of list) {
result.unshift(bean.toJSON())
}
socket.emit("heartbeatList", monitorID, result)
}

View File

@ -1,3 +1,12 @@
exports.sleep = (ms) => {
/*
* Common functions - can be used in frontend or backend
*/
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function ucfirst(str) {
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}

View File

@ -31,4 +31,8 @@
}
}
.modal-content {
border-radius: 1rem;
backdrop-filter: blur(3px);
}

View File

@ -1,7 +1,13 @@
<template>
<div class="wrap" :style="wrapStyle" ref="wrap">
<div class="hp-bar-big" :style="barStyle">
<div class="beat" :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }" :style="beatStyle" v-for="(beat, index) in shortBeatList" :key="index">
<div
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
:style="beatStyle"
v-for="(beat, index) in shortBeatList"
:key="index"
:title="beat.msg">
</div>
</div>
</div>

View File

@ -0,0 +1,83 @@
<template>
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="submit">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select class="form-select" id="type" v-model="type">
<option value="email">Email</option>
<option value="webhook">Webhook</option>
<option value="telegram">Telegram</option>
<option value="discord">Discord</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input type="text" class="form-control" id="name" required v-model="name">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" @click="yes" data-bs-dismiss="modal">Save</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
import { ucfirst } from "../../server/util";
export default {
props: {
},
data() {
return {
model: null,
type: null,
name: "",
}
},
mounted() {
this.modal = new Modal(this.$refs.modal)
},
methods: {
show() {
this.modal.show()
},
submit() {
}
},
watch: {
type(to, from) {
let oldName;
if (from) {
oldName = `My ${ucfirst(from)} Notification`;
} else {
oldName = "";
}
if (! this.name || this.name === oldName) {
this.name = `My ${ucfirst(to)} Alert (1)`
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -13,15 +13,14 @@ export default {
token: null,
firstConnect: true,
connected: false,
connectCount: 0,
},
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
loggedIn: false,
monitorList: [
],
importantHeartbeatList: [
],
heartbeatList: {
},
@ -38,7 +37,6 @@ export default {
});
socket.on('heartbeat', (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];
}
@ -46,14 +44,30 @@ export default {
this.heartbeatList[data.monitorID].push(data)
});
socket.on('heartbeatList', (monitorID, data) => {
if (! (monitorID in this.heartbeatList)) {
this.heartbeatList[monitorID] = data;
} else {
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
}
});
socket.on('disconnect', () => {
console.log("disconnect")
this.socket.connected = false;
});
socket.on('connect', () => {
console.log("connect")
this.socket.connectCount++;
this.socket.connected = true;
this.socket.firstConnect = false;
// Reset Heartbeat list if it is re-connect
if (this.socket.connectCount >= 2) {
console.log("reset heartbeat list")
this.heartbeatList = {}
}
if (storage.token) {
this.loginByToken(storage.token)
@ -61,6 +75,7 @@ export default {
this.allowLoginDialog = true;
}
this.socket.firstConnect = false;
});
},
@ -113,10 +128,9 @@ export default {
logout() {
storage.removeItem("token");
this.socket.token = null;
this.loggedIn = false;
socket.emit("logout", () => {
toast.success("Logout Successfully")
window.location.reload()
})
},
@ -142,6 +156,26 @@ export default {
return result;
},
// TODO: handle history + real time
importantHeartbeatList() {
let result = {}
for (let monitorID in this.heartbeatList) {
result[monitorID] = [];
let index = this.heartbeatList[monitorID].length - 1;
let list = this.heartbeatList[monitorID];
for (let heartbeat of list) {
if (heartbeat.important) {
result[monitorID].push(heartbeat)
}
}
}
return result;
},
statusList() {
let result = {}

View File

@ -134,8 +134,6 @@ export default {
result.unknown++;
}
} else {
console.log(monitorID + " Unknown?")
console.log(beat)
result.unknown++;
}
}

View File

@ -40,23 +40,24 @@
<div class="col-md-6">
<h2>Notifications</h2>
<p>Not available, please setup in Settings page.</p>
<a class="btn btn-primary me-2" href="/settings" target="_blank">Go to Settings</a>
<p>Not available, please setup.</p>
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
</div>
</div>
</div>
</form>
<NotificationDialog ref="notificationDialog" />
</template>
<script>
import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from 'vue-toastification'
const toast = useToast()
export default {
components: {
NotificationDialog
},
mounted() {
this.init();