diff --git a/package.json b/package.json index 107cf1ae..867af2b1 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "scripts": { "dev": "vite", + "dev-server": "node server/server.js", "build": "vite build", "serve": "vite preview" }, diff --git a/server/model/monitor.js b/server/model/monitor.js index 1b25270f..9c491146 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,6 +1,18 @@ 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 Monitor extends BeanModel { toJSON() { @@ -16,9 +28,36 @@ class Monitor extends BeanModel { } start(io) { - const beat = () => { + const beat = async () => { console.log(`Monitor ${this.id}: Heartbeat`) - io.to(this.user_id).emit("heartbeat", dayjs().unix()); + + let bean = R.dispense("heartbeat") + bean.monitor_id = this.id; + bean.time = R.isoDateTime(dayjs.utc()); + bean.status = 0; + + try { + if (this.type === "http") { + let startTime = dayjs().valueOf(); + let res = await axios.get(this.url) + bean.msg = `${res.status} - ${res.statusText}` + bean.ping = dayjs().valueOf() - startTime; + bean.status = 1; + } + + } catch (error) { + 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, + }); + + await R.store(bean) } beat(); diff --git a/server/server.js b/server/server.js index e30f6e7e..e9f62c49 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,6 @@ const http = require('http'); const server = http.createServer(app); const { Server } = require("socket.io"); const io = new Server(server); -const axios = require('axios'); const dayjs = require("dayjs"); const {R} = require("redbean-node"); const passwordHash = require('password-hash'); @@ -12,7 +11,6 @@ const jwt = require('jsonwebtoken'); const Monitor = require("./model/monitor"); const {sleep} = require("./util"); - let stop = false; let interval = 6000; let totalClient = 0; @@ -21,12 +19,11 @@ let loadFromDatabase = true; let monitorList = {}; (async () => { - R.setup('sqlite', { - filename: '../data/kuma.db' + filename: './data/kuma.db' }); R.freeze(true) - await R.autoloadModels("./model"); + await R.autoloadModels("./server/model"); await initDatabase(); @@ -118,15 +115,54 @@ let monitorList = {}; bean.user_id = socket.userID await R.store(bean) + await startMonitor(socket.userID, bean.id); + await sendMonitorList(socket); + callback({ ok: true, msg: "Added Successfully.", monitorID: bean.id }); + } catch (e) { + callback({ + ok: false, + msg: e.message + }); + } + }); + + socket.on("editMonitor", async (monitor, callback) => { + try { + checkLogin(socket) + + let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]) + + if (bean.user_id !== socket.userID) { + throw new Error("Permission denied.") + } + + bean.name = monitor.name + bean.type = monitor.type + bean.url = monitor.url + bean.interval = monitor.interval + + await R.store(bean) + + if (bean.active) { + await restartMonitor(socket.userID, bean.id) + } + await sendMonitorList(socket); + callback({ + ok: true, + msg: "Saved.", + monitorID: bean.id + }); + } catch (e) { + console.log(e) callback({ ok: false, msg: e.message @@ -294,14 +330,14 @@ async function afterLogin(socket, user) { } async function getMonitorJSONList(userID) { - let result = []; + let result = {}; - let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC ", [ + let monitorList = await R.find("monitor", " user_id = ? ORDER BY active DESC, name ASC ", [ userID ]) for (let monitor of monitorList) { - result.push(monitor.toJSON()) + result[monitor.id] = monitor.toJSON(); } return result; @@ -346,10 +382,18 @@ async function startMonitor(userID, monitorID) { monitorID ]) + if (monitor.id in monitorList) { + monitorList[monitor.id].stop(); + } + monitorList[monitor.id] = monitor; monitor.start(io) } +async function restartMonitor(userID, monitorID) { + return await startMonitor(userID, monitorID) +} + async function pauseMonitor(userID, monitorID) { await checkOwner(userID, monitorID) diff --git a/src/assets/vars.scss b/src/assets/vars.scss index dd1bcc0b..31b0262d 100644 --- a/src/assets/vars.scss +++ b/src/assets/vars.scss @@ -1,4 +1,5 @@ $primary: #5CDD8B; +$danger: #DC3545; $link-color: #111; $border-radius: 50rem; diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index bba0fa54..ede199ee 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -1,7 +1,7 @@ <template> <div class="wrap" :style="wrapStyle" ref="wrap"> <div class="hp-bar-big" :style="barStyle"> - <div class="beat" :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"> </div> </div> </div> @@ -11,12 +11,15 @@ export default { + props: { + size: { + type: String, + default: "big" + }, + monitorId: Number + }, data() { return { - i: 1, - beatList: [ - - ], beatWidth: 10, beatHeight: 30, hoverScale: 1.5, @@ -25,26 +28,38 @@ export default { maxBeat: -1, } }, - destroyed() { + unmounted() { window.removeEventListener("resize", this.resize); }, mounted() { + if (this.size === "small") { + this.beatWidth = 5.6; + this.beatMargin = 2.4; + this.beatHeight = 16 + } + window.addEventListener("resize", this.resize); this.resize(); - - setInterval(() => { - this.beatList.push(this.i++) - }, 3000) - }, methods: { resize() { - this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)) + if (this.$refs.wrap) { + this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)) + } } }, computed: { + beatList() { + if (! (this.monitorId in this.$root.heartbeatList)) { + this.$root.heartbeatList[this.monitorId] = []; + } + return this.$root.heartbeatList[this.monitorId] + }, + shortBeatList() { + let placeholders = [] + let start = this.beatList.length - this.maxBeat; if (this.move) { @@ -52,10 +67,16 @@ export default { } if (start < 0) { + // Add empty placeholder + for (let i = start; i < 0; i++) { + placeholders.push(0) + } start = 0; } - return this.beatList.slice(start) + + + return placeholders.concat(this.beatList.slice(start)) }, wrapStyle() { @@ -104,7 +125,6 @@ export default { watch: { beatList: { handler(val, oldVal) { - console.log("add beat2") this.move = true; setTimeout(() => { @@ -131,14 +151,17 @@ export default { display: inline-block; background-color: $primary; border-radius: 50rem; - transition: all ease-in-out 0.15s; - &.new-beat { + &.empty { background-color: aliceblue; } + &.down { + background-color: $danger; + } - &:hover { + &:not(.empty):hover { + transition: all ease-in-out 0.15s; opacity: 0.8; transform: scale(var(--hover-scale)); } diff --git a/src/layouts/EmptyLayout.vue b/src/layouts/EmptyLayout.vue new file mode 100644 index 00000000..7b720447 --- /dev/null +++ b/src/layouts/EmptyLayout.vue @@ -0,0 +1,13 @@ +<template> + <router-view /> +</template> + +<script> +export default { + +} +</script> + +<style scoped> + +</style> diff --git a/src/main.js b/src/main.js index 085c25f2..112512e5 100644 --- a/src/main.js +++ b/src/main.js @@ -3,6 +3,7 @@ import {createRouter, createWebHistory} from 'vue-router' import App from './App.vue' import Layout from './layouts/Layout.vue' +import EmptyLayout from './layouts/EmptyLayout.vue' import Settings from "./pages/Settings.vue"; import Dashboard from "./pages/Dashboard.vue"; import DashboardHome from "./pages/DashboardHome.vue"; @@ -30,17 +31,23 @@ const routes = [ component: DashboardHome, children: [ { - path: ':id', - component: Details, + path: '/dashboard/:id', + component: EmptyLayout, + children: [ + { + path: '', + component: Details, + }, + { + path: '/edit/:id', + component: EditMonitor, + }, + ] }, { path: '/add', component: EditMonitor, }, - { - path: '/edit/:id', - component: EditMonitor, - }, ] }, { diff --git a/src/mixins/socket.js b/src/mixins/socket.js index a55a1d3e..1592e700 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -21,7 +21,10 @@ export default { ], importantHeartbeatList: [ - ] + ], + heartbeatList: { + + }, } }, @@ -34,6 +37,16 @@ export default { this.monitorList = data; }); + socket.on('heartbeat', (data) => { + + if (! (data.monitorID in this.heartbeatList)) { + this.heartbeatList[data.monitorID] = []; + } + + this.heartbeatList[data.monitorID].push(data) + }); + + socket.on('disconnect', () => { this.socket.connected = false; }); @@ -53,9 +66,11 @@ export default { }, methods: { + getSocket() { return socket; }, + toastRes(res) { if (res.ok) { toast.success(res.msg); @@ -63,6 +78,7 @@ export default { toast.error(res.msg); } }, + login(username, password, callback) { socket.emit("login", { username, @@ -81,18 +97,19 @@ export default { callback(res) }) }, + loginByToken(token) { socket.emit("loginByToken", token, (res) => { this.allowLoginDialog = true; if (! res.ok) { this.logout() - console.log(res.msg) } else { this.loggedIn = true; } }) }, + logout() { storage.removeItem("token"); this.socket.token = null; @@ -102,19 +119,59 @@ export default { toast.success("Logout Successfully") }) }, + add(monitor, callback) { socket.emit("add", monitor, callback) }, + deleteMonitor(monitorID, callback) { socket.emit("deleteMonitor", monitorID, callback) }, - loadMonitor(monitorID) { - } }, computed: { + lastHeartbeatList() { + let result = {} + for (let monitorID in this.heartbeatList) { + let index = this.heartbeatList[monitorID].length - 1; + result[monitorID] = this.heartbeatList[monitorID][index]; + } + + return result; + }, + + statusList() { + let result = {} + + let unknown = { + text: "Unknown", + color: "secondary" + } + + for (let monitorID in this.lastHeartbeatList) { + let lastHeartBeat = this.lastHeartbeatList[monitorID] + + if (! lastHeartBeat) { + result[monitorID] = unknown; + } else if (lastHeartBeat.status === 1) { + result[monitorID] = { + text: "Up", + color: "primary" + }; + } else if (lastHeartBeat.status === 0) { + result[monitorID] = { + text: "Down", + color: "danger" + }; + } else { + result[monitorID] = unknown; + } + } + + return result; + } } } diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue index 25903f0b..87950b6d 100644 --- a/src/pages/Dashboard.vue +++ b/src/pages/Dashboard.vue @@ -23,20 +23,7 @@ </div> <div class="col-6"> - <div class="hp-bar"> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - </div> + <HeartbeatBar size="small" :monitor-id="item.id" /> </div> </div> @@ -54,8 +41,11 @@ <script> +import HeartbeatBar from "../components/HeartbeatBar.vue"; + export default { components: { + HeartbeatBar }, data() { return { @@ -78,6 +68,8 @@ export default { .list { margin-top: 25px; + height: auto; + min-height: calc(100vh - 200px); .item { display: block; @@ -92,6 +84,8 @@ export default { .info { white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &:hover { @@ -118,6 +112,10 @@ export default { border-radius: 50rem; transition: all ease-in-out 0.15s; + &.empty { + background-color: aliceblue; + } + &:hover { opacity: 0.8; transform: scale(1.5); diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue index b9ecfefb..80b1db71 100644 --- a/src/pages/DashboardHome.vue +++ b/src/pages/DashboardHome.vue @@ -61,15 +61,19 @@ <div class="col"> <h3>Up</h3> - <span class="num">2</span> + <span class="num">{{ stats.up }}</span> </div> <div class="col"> <h3>Down</h3> - <span class="num text-danger">0</span> + <span class="num text-danger">{{ stats.down }}</span> + </div> + <div class="col"> + <h3>Unknown</h3> + <span class="num text-secondary">{{ stats.unknown }}</span> </div> <div class="col"> <h3>Pause</h3> - <span class="num">0</span> + <span class="num text-secondary">{{ stats.pause }}</span> </div> </div> </div> @@ -107,7 +111,37 @@ <script> export default { computed: { + stats() { + let result = { + up: 0, + down: 0, + unknown: 0, + pause: 0, + }; + for (let monitorID in this.$root.monitorList) { + let beat = this.$root.lastHeartbeatList[monitorID]; + let monitor = this.$root.monitorList[monitorID] + + if (monitor && ! monitor.active) { + result.pause++; + } else if (beat) { + if (beat.status === 1) { + result.up++; + } else if (beat.status === 0) { + result.down++; + } else { + result.unknown++; + } + } else { + console.log(monitorID + " Unknown?") + console.log(beat) + result.unknown++; + } + } + + return result; + }, } } </script> diff --git a/src/pages/Details.vue b/src/pages/Details.vue index c5d263d9..ffd70494 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -1,6 +1,6 @@ <template> - <h1>{{ monitor.name }}</h1> - <h2>{{ monitor.url }}</h2> + <h1> {{ monitor.name }}</h1> + <p class="url"><a :href="monitor.url" target="_blank" v-if="monitor.type === 'http'">{{ monitor.url }}</a></p> <div class="functions"> <button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> @@ -11,14 +11,15 @@ <div class="shadow-box"> - <HeartbeatBar /> + <div class="row"> <div class="col-md-8"> - + <HeartbeatBar :monitor-id="monitor.id" /> + <span class="word">Check every {{ monitor.interval }} seconds.</span> </div> - <div class="col-md-4"> - + <div class="col-md-4 text-center"> + <span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span> </div> </div> </div> @@ -53,15 +54,28 @@ export default { }, computed: { monitor() { - let id = parseInt(this.$route.params.id) + let id = this.$route.params.id + return this.$root.monitorList[id]; + }, + + lastHeartBeat() { + if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { + return this.$root.lastHeartbeatList[this.monitor.id] + } else { + return { status: -1 } + } + }, + + status() { + if (this.$root.statusList[this.monitor.id]) { + return this.$root.statusList[this.monitor.id] + } else { + return { - for (let monitor of this.$root.monitorList) { - if (monitor.id === id) { - return monitor; } } - return {}; - }, + } + }, methods: { pauseDialog() { @@ -97,9 +111,14 @@ export default { <style lang="scss" scoped> @import "../assets/vars.scss"; -h2 { +.url { color: $primary; margin-bottom: 20px; + font-weight: bold; + + a { + color: $primary; + } } .functions { @@ -112,4 +131,9 @@ h2 { padding: 20px; margin-top: 25px; } + +.word { + color: #AAA; + font-size: 14px; +} </style> diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 143806a7..1bc16bb8 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -29,7 +29,7 @@ <div class="mb-3"> <label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> - <input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="20"> + <input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20"> </div> <div> @@ -59,24 +59,7 @@ export default { }, mounted() { - - if (this.isAdd) { - this.monitor = { - type: "http", - name: "", - url: "https://", - interval: 60, - } - } else { - this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { - if (res.ok) { - this.monitor = res.monitor; - } else { - toast.error(res.msg) - } - }) - } - + this.init(); }, data() { return { @@ -90,9 +73,33 @@ export default { }, isAdd() { return this.$route.path === "/add"; + }, + isEdit() { + return this.$route.path.startsWith("/edit"); } }, methods: { + init() { + if (this.isAdd) { + console.log("??????") + this.monitor = { + type: "http", + name: "", + url: "https://", + interval: 60, + } + } else if (this.isEdit) { + this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { + if (res.ok) { + this.monitor = res.monitor; + } else { + toast.error(res.msg) + } + }) + } + + }, + submit() { this.processing = true; @@ -109,10 +116,18 @@ export default { }) } else { - + this.$root.getSocket().emit("editMonitor", this.monitor, (res) => { + this.processing = false; + this.$root.toastRes(res) + }) } } - } + }, + watch: { + '$route.fullPath' () { + this.init(); + } + }, } </script>