17 changed files with 1533 additions and 0 deletions
			
			
		| @ -0,0 +1,33 @@ | |||||
|  | const dayjs = require("dayjs"); | ||||
|  | const {BeanModel} = require("redbean-node/dist/bean-model"); | ||||
|  | 
 | ||||
|  | class Monitor extends BeanModel { | ||||
|  | 
 | ||||
|  |     toJSON() { | ||||
|  |         return { | ||||
|  |             id: this.id, | ||||
|  |             name: this.name, | ||||
|  |             url: this.url, | ||||
|  |             upRate: this.upRate, | ||||
|  |             active: this.active, | ||||
|  |             type: this.type, | ||||
|  |             interval: this.interval, | ||||
|  |         }; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     start(io) { | ||||
|  |         const beat = () => { | ||||
|  |             console.log(`Monitor ${this.id}: Heartbeat`) | ||||
|  |             io.to(this.user_id).emit("heartbeat", dayjs().unix()); | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         beat(); | ||||
|  |         this.heartbeatInterval = setInterval(beat, this.interval * 1000); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     stop() { | ||||
|  |         clearInterval(this.heartbeatInterval) | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | module.exports = Monitor; | ||||
| @ -0,0 +1,379 @@ | |||||
|  | const express = require('express'); | ||||
|  | const app = express(); | ||||
|  | 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'); | ||||
|  | const jwt = require('jsonwebtoken'); | ||||
|  | const Monitor = require("./model/monitor"); | ||||
|  | const {sleep} = require("./util"); | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | let stop = false; | ||||
|  | let interval = 6000; | ||||
|  | let totalClient = 0; | ||||
|  | let jwtSecret = null; | ||||
|  | let loadFromDatabase = true; | ||||
|  | let monitorList = {}; | ||||
|  | 
 | ||||
|  | (async () => { | ||||
|  | 
 | ||||
|  |     R.setup('sqlite', { | ||||
|  |         filename: '../data/kuma.db' | ||||
|  |     }); | ||||
|  |     R.freeze(true) | ||||
|  |     await R.autoloadModels("./model"); | ||||
|  | 
 | ||||
|  |     await initDatabase(); | ||||
|  | 
 | ||||
|  |     app.use('/', express.static("public")); | ||||
|  | 
 | ||||
|  |     io.on('connection', async (socket) => { | ||||
|  |         console.log('a user connected'); | ||||
|  |         totalClient++; | ||||
|  | 
 | ||||
|  |         socket.on('disconnect', () => { | ||||
|  |             console.log('user disconnected'); | ||||
|  |             totalClient--; | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         // Public API
 | ||||
|  | 
 | ||||
|  |         socket.on("loginByToken", async (token, callback) => { | ||||
|  | 
 | ||||
|  |             try { | ||||
|  |                 let decoded = jwt.verify(token, jwtSecret); | ||||
|  | 
 | ||||
|  |                 console.log("Username from JWT: " + decoded.username) | ||||
|  | 
 | ||||
|  |                 let user = await R.findOne("user", " username = ? AND active = 1 ", [ | ||||
|  |                     decoded.username | ||||
|  |                 ]) | ||||
|  | 
 | ||||
|  |                 if (user) { | ||||
|  |                     await afterLogin(socket, user) | ||||
|  | 
 | ||||
|  |                     callback({ | ||||
|  |                         ok: true, | ||||
|  |                     }) | ||||
|  |                 } else { | ||||
|  |                     callback({ | ||||
|  |                         ok: false, | ||||
|  |                         msg: "The user is inactive or deleted." | ||||
|  |                     }) | ||||
|  |                 } | ||||
|  |             } catch (error) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: "Invalid token." | ||||
|  |                 }) | ||||
|  |             } | ||||
|  | 
 | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("login", async (data, callback) => { | ||||
|  |             console.log("Login") | ||||
|  | 
 | ||||
|  |             let user = await R.findOne("user", " username = ? AND active = 1 ", [ | ||||
|  |                 data.username | ||||
|  |             ]) | ||||
|  | 
 | ||||
|  |             if (user && passwordHash.verify(data.password, user.password)) { | ||||
|  | 
 | ||||
|  |                 await afterLogin(socket, user) | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     token: jwt.sign({ | ||||
|  |                         username: data.username | ||||
|  |                     }, jwtSecret) | ||||
|  |                 }) | ||||
|  |             } else { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: "Incorrect username or password." | ||||
|  |                 }) | ||||
|  |             } | ||||
|  | 
 | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("logout", async (callback) => { | ||||
|  |             socket.leave(socket.userID) | ||||
|  |             socket.userID = null; | ||||
|  |             callback(); | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         // Auth Only API
 | ||||
|  | 
 | ||||
|  |         socket.on("add", async (monitor, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  | 
 | ||||
|  |                 let bean = R.dispense("monitor") | ||||
|  |                 bean.import(monitor) | ||||
|  |                 bean.user_id = socket.userID | ||||
|  |                 await R.store(bean) | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     msg: "Added Successfully.", | ||||
|  |                     monitorID: bean.id | ||||
|  |                 }); | ||||
|  | 
 | ||||
|  |                 await sendMonitorList(socket); | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("getMonitor", async (monitorID, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  | 
 | ||||
|  |                 console.log(`Get Monitor: ${monitorID} User ID: ${socket.userID}`) | ||||
|  | 
 | ||||
|  |                 let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [ | ||||
|  |                     monitorID, | ||||
|  |                     socket.userID, | ||||
|  |                 ]) | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     monitor: bean.toJSON(), | ||||
|  |                 }); | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         // Start or Resume the monitor
 | ||||
|  |         socket.on("resumeMonitor", async (monitorID, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  |                 await startMonitor(socket.userID, monitorID); | ||||
|  |                 await sendMonitorList(socket); | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     msg: "Paused Successfully." | ||||
|  |                 }); | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("pauseMonitor", async (monitorID, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  |                 await pauseMonitor(socket.userID, monitorID) | ||||
|  |                 await sendMonitorList(socket); | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     msg: "Paused Successfully." | ||||
|  |                 }); | ||||
|  | 
 | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("deleteMonitor", async (monitorID, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  | 
 | ||||
|  |                 console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`) | ||||
|  | 
 | ||||
|  |                 if (monitorID in monitorList) { | ||||
|  |                     monitorList[monitorID].stop(); | ||||
|  |                     delete monitorList[monitorID] | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ | ||||
|  |                     monitorID, | ||||
|  |                     socket.userID | ||||
|  |                 ]); | ||||
|  | 
 | ||||
|  |                 callback({ | ||||
|  |                     ok: true, | ||||
|  |                     msg: "Deleted Successfully." | ||||
|  |                 }); | ||||
|  | 
 | ||||
|  |                 await sendMonitorList(socket); | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on("changePassword", async (password, callback) => { | ||||
|  |             try { | ||||
|  |                 checkLogin(socket) | ||||
|  | 
 | ||||
|  |                 if (! password.currentPassword) { | ||||
|  |                     throw new Error("Invalid new password") | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 let user = await R.findOne("user", " id = ? AND active = 1 ", [ | ||||
|  |                     socket.userID | ||||
|  |                 ]) | ||||
|  | 
 | ||||
|  |                 if (user && passwordHash.verify(password.currentPassword, user.password)) { | ||||
|  | 
 | ||||
|  |                     await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ | ||||
|  |                         passwordHash.generate(password.newPassword), | ||||
|  |                         socket.userID | ||||
|  |                     ]); | ||||
|  | 
 | ||||
|  |                     callback({ | ||||
|  |                         ok: true, | ||||
|  |                         msg: "Password has been updated successfully." | ||||
|  |                     }) | ||||
|  |                 } else { | ||||
|  |                     throw new Error("Incorrect current password") | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |             } catch (e) { | ||||
|  |                 callback({ | ||||
|  |                     ok: false, | ||||
|  |                     msg: e.message | ||||
|  |                 }); | ||||
|  |             } | ||||
|  |         }); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  |     server.listen(3001, () => { | ||||
|  |         console.log('Listening on 3001'); | ||||
|  |         startMonitors(); | ||||
|  |     }); | ||||
|  | 
 | ||||
|  | })(); | ||||
|  | 
 | ||||
|  | async function checkOwner(userID, monitorID) { | ||||
|  |     let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [ | ||||
|  |         monitorID, | ||||
|  |         userID, | ||||
|  |     ]) | ||||
|  | 
 | ||||
|  |     if (! row) { | ||||
|  |         throw new Error("You do not own this monitor."); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function sendMonitorList(socket) { | ||||
|  |     io.to(socket.userID).emit("monitorList", await getMonitorJSONList(socket.userID)) | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function afterLogin(socket, user) { | ||||
|  |     socket.userID = user.id; | ||||
|  |     socket.join(user.id) | ||||
|  |     socket.emit("monitorList", await getMonitorJSONList(user.id)) | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function getMonitorJSONList(userID) { | ||||
|  |     let result = []; | ||||
|  | 
 | ||||
|  |     let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC ", [ | ||||
|  |         userID | ||||
|  |     ]) | ||||
|  | 
 | ||||
|  |     for (let monitor of monitorList) { | ||||
|  |         result.push(monitor.toJSON()) | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     return result; | ||||
|  | } | ||||
|  | 
 | ||||
|  | function checkLogin(socket) { | ||||
|  |     if (! socket.userID) { | ||||
|  |         throw new Error("You are not logged in."); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function initDatabase() { | ||||
|  |     let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ | ||||
|  |         "jwtSecret" | ||||
|  |     ]); | ||||
|  | 
 | ||||
|  |     if (! jwtSecretBean) { | ||||
|  |         console.log("JWT secret is not found, generate one.") | ||||
|  |         jwtSecretBean = R.dispense("setting") | ||||
|  |         jwtSecretBean.key = "jwtSecret" | ||||
|  | 
 | ||||
|  |         jwtSecretBean.value = passwordHash.generate(dayjs() + "") | ||||
|  |         await R.store(jwtSecretBean) | ||||
|  |     } else { | ||||
|  |         console.log("Load JWT secret from database.") | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     jwtSecret = jwtSecretBean.value; | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function startMonitor(userID, monitorID) { | ||||
|  |     await checkOwner(userID, monitorID) | ||||
|  | 
 | ||||
|  |     console.log(`Resume Monitor: ${monitorID} User ID: ${userID}`) | ||||
|  | 
 | ||||
|  |     await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [ | ||||
|  |         monitorID, | ||||
|  |         userID | ||||
|  |     ]); | ||||
|  | 
 | ||||
|  |     let monitor = await R.findOne("monitor", " id = ? ", [ | ||||
|  |         monitorID | ||||
|  |     ]) | ||||
|  | 
 | ||||
|  |     monitorList[monitor.id] = monitor; | ||||
|  |     monitor.start(io) | ||||
|  | } | ||||
|  | 
 | ||||
|  | async function pauseMonitor(userID, monitorID) { | ||||
|  |     await checkOwner(userID, monitorID) | ||||
|  | 
 | ||||
|  |     console.log(`Pause Monitor: ${monitorID} User ID: ${userID}`) | ||||
|  | 
 | ||||
|  |     await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [ | ||||
|  |         monitorID, | ||||
|  |         userID | ||||
|  |     ]); | ||||
|  | 
 | ||||
|  |     if (monitorID in monitorList) { | ||||
|  |         monitorList[monitorID].stop(); | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  * Resume active monitors | ||||
|  |  */ | ||||
|  | async function startMonitors() { | ||||
|  |     let list = await R.find("monitor", " active = 1 ") | ||||
|  | 
 | ||||
|  |     for (let monitor of list) { | ||||
|  |         monitor.start(io) | ||||
|  |         monitorList[monitor.id] = monitor; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
| @ -0,0 +1,3 @@ | |||||
|  | exports.sleep = (ms) => { | ||||
|  |     return new Promise(resolve => setTimeout(resolve, ms)); | ||||
|  | } | ||||
| @ -0,0 +1,13 @@ | |||||
|  | <template> | ||||
|  |     <router-view /> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | export default { | ||||
|  | 
 | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style lang="scss"> | ||||
|  | 
 | ||||
|  | </style> | ||||
| @ -0,0 +1,57 @@ | |||||
|  | @import "vars.scss"; | ||||
|  | @import "node_modules/bootstrap/scss/bootstrap"; | ||||
|  | 
 | ||||
|  | #app { | ||||
|  |     font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .shadow-box { | ||||
|  |     overflow: hidden; | ||||
|  |     box-shadow: 0 15px 70px rgba(0, 0, 0, .1); | ||||
|  |     padding: 10px; | ||||
|  |     border-radius: 10px; | ||||
|  | 
 | ||||
|  |     &.big-padding { | ||||
|  |         padding: 20px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .btn { | ||||
|  |     padding-left: 20px; | ||||
|  |     padding-right: 20px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .btn-primary { | ||||
|  |     color: white; | ||||
|  | 
 | ||||
|  |     &:hover, &:active, &:focus, &.active { | ||||
|  |         color: white; | ||||
|  |         background-color: $highlight; | ||||
|  |         border-color: $highlight; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .hp-bar-big { | ||||
|  |     white-space: nowrap; | ||||
|  |     margin-top: 4px; | ||||
|  |     text-align: center; | ||||
|  |     direction: rtl; | ||||
|  |     margin-bottom: 10px; | ||||
|  |     transition: all ease-in-out 0.15s; | ||||
|  |     position: relative; | ||||
|  | 
 | ||||
|  |     div { | ||||
|  |         display: inline-block; | ||||
|  |         background-color: $primary; | ||||
|  |         width: 1%; | ||||
|  |         height: 30px; | ||||
|  |         margin: 0.3%; | ||||
|  |         border-radius: 50rem; | ||||
|  |         transition: all ease-in-out 0.15s; | ||||
|  | 
 | ||||
|  |         &:hover { | ||||
|  |             opacity: 0.8; | ||||
|  |             transform: scale(1.5); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
| @ -0,0 +1,6 @@ | |||||
|  | $primary: #5CDD8B; | ||||
|  | $link-color: #111; | ||||
|  | $border-radius: 50rem; | ||||
|  | 
 | ||||
|  | $highlight: #7ce8a4; | ||||
|  | $highlight-white: #e7faec; | ||||
| @ -0,0 +1,50 @@ | |||||
|  | <template> | ||||
|  |     <div class="modal fade" tabindex="-1" ref="modal"> | ||||
|  |         <div class="modal-dialog"> | ||||
|  |             <div class="modal-content"> | ||||
|  |                 <div class="modal-header"> | ||||
|  |                     <h5 class="modal-title" id="exampleModalLabel">Confirm</h5> | ||||
|  |                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||
|  |                 </div> | ||||
|  |                 <div class="modal-body"> | ||||
|  |                     <slot></slot> | ||||
|  |                 </div> | ||||
|  |                 <div class="modal-footer"> | ||||
|  |                     <button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button> | ||||
|  |                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> | ||||
|  |                 </div> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import { Modal } from 'bootstrap' | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     props: { | ||||
|  |         btnStyle: { | ||||
|  |             type: String, | ||||
|  |             default: "btn-primary" | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     data: () => ({ | ||||
|  |         modal: null | ||||
|  |     }), | ||||
|  |     mounted() { | ||||
|  |         this.modal = new Modal(this.$refs.modal) | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         show() { | ||||
|  |             this.modal.show() | ||||
|  |         }, | ||||
|  |         yes() { | ||||
|  |             this.$emit('yes'); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped> | ||||
|  | 
 | ||||
|  | </style> | ||||
| @ -0,0 +1,77 @@ | |||||
|  | <template> | ||||
|  |     <div class="form-container"> | ||||
|  |         <div class="form"> | ||||
|  |             <form @submit.prevent="submit"> | ||||
|  | 
 | ||||
|  |                 <h1 class="h3 mb-3 fw-normal"></h1> | ||||
|  | 
 | ||||
|  |                 <div class="form-floating"> | ||||
|  |                     <input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username"> | ||||
|  |                     <label for="floatingInput">Username</label> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div class="form-floating mt-3"> | ||||
|  |                     <input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password"> | ||||
|  |                     <label for="floatingPassword">Password</label> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div class="form-check mb-3 mt-3"> | ||||
|  |                     <label> | ||||
|  |                         <input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="remember"> | ||||
|  | 
 | ||||
|  |                         <label class="form-check-label" for="remember"> | ||||
|  |                             Remember me | ||||
|  |                         </label> | ||||
|  |                     </label> | ||||
|  |                 </div> | ||||
|  |                 <button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button> | ||||
|  | 
 | ||||
|  |                 <div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok"> | ||||
|  |                     {{ res.msg }} | ||||
|  |                 </div> | ||||
|  |             </form> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | export default { | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             processing: false, | ||||
|  |             username: "", | ||||
|  |             password: "", | ||||
|  |             remember: true, | ||||
|  |             res: null, | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         submit() { | ||||
|  |             this.processing = true; | ||||
|  |             this.$root.login(this.username, this.password, (res) => { | ||||
|  |                 this.processing = false; | ||||
|  |                 this.res = res; | ||||
|  |             }) | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped> | ||||
|  | 
 | ||||
|  | .form-container { | ||||
|  |     display: flex; | ||||
|  |     align-items: center; | ||||
|  |     padding-top: 40px; | ||||
|  |     padding-bottom: 40px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .form { | ||||
|  | 
 | ||||
|  |     width: 100%; | ||||
|  |     max-width: 330px; | ||||
|  |     padding: 15px; | ||||
|  |     margin: auto; | ||||
|  |     text-align: center; | ||||
|  | } | ||||
|  | </style> | ||||
| @ -0,0 +1,69 @@ | |||||
|  | <template> | ||||
|  | 
 | ||||
|  |     <div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect"> | ||||
|  |         <div class="container-fluid"> | ||||
|  |             Lost connection to the socket server. Reconnecting... | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  |     <header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom"> | ||||
|  | 
 | ||||
|  |         <router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> | ||||
|  |             <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg> | ||||
|  |             <span class="fs-4 title">Uptime Kuma</span> | ||||
|  |         </router-link> | ||||
|  | 
 | ||||
|  |         <ul class="nav nav-pills"> | ||||
|  |             <li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li> | ||||
|  |             <li class="nav-item"><router-link to="/settings" class="nav-link">⚙ Settings</router-link></li> | ||||
|  |         </ul> | ||||
|  | 
 | ||||
|  |     </header> | ||||
|  | 
 | ||||
|  |     <main> | ||||
|  |         <router-view v-if="$root.loggedIn" /> | ||||
|  |         <Login v-if="! $root.loggedIn && $root.allowLoginDialog" /> | ||||
|  |     </main> | ||||
|  | 
 | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import Login from "../components/Login.vue"; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  |         Login | ||||
|  |     }, | ||||
|  |     mounted() { | ||||
|  |         this.init(); | ||||
|  |     }, | ||||
|  |     watch: { | ||||
|  |         $route (to, from) { | ||||
|  |             this.init(); | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         init() { | ||||
|  |             if (this.$route.name === "root") { | ||||
|  |                 this.$router.push("/dashboard") | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped> | ||||
|  |     .title { | ||||
|  |         font-weight: bold; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .nav { | ||||
|  |         margin-right: 25px; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     .lost-connection { | ||||
|  |         padding: 5px; | ||||
|  |         background-color: crimson; | ||||
|  |         color: white; | ||||
|  |     } | ||||
|  | </style> | ||||
| @ -0,0 +1,78 @@ | |||||
|  | import {createApp, h} from "vue"; | ||||
|  | import {createRouter, createWebHistory} from 'vue-router' | ||||
|  | 
 | ||||
|  | import App from './App.vue' | ||||
|  | import Layout from './layouts/Layout.vue' | ||||
|  | import Settings from "./pages/Settings.vue"; | ||||
|  | import Dashboard from "./pages/Dashboard.vue"; | ||||
|  | import DashboardHome from "./pages/DashboardHome.vue"; | ||||
|  | import Details from "./pages/Details.vue"; | ||||
|  | import socket from "./mixins/socket" | ||||
|  | import "./assets/app.scss" | ||||
|  | import EditMonitor from "./pages/EditMonitor.vue"; | ||||
|  | import Toast from "vue-toastification"; | ||||
|  | import "vue-toastification/dist/index.css"; | ||||
|  | import "bootstrap" | ||||
|  | 
 | ||||
|  | const routes = [ | ||||
|  |     { | ||||
|  |         path: '/', | ||||
|  |         component: Layout, | ||||
|  |         children: [ | ||||
|  |             { | ||||
|  |                 name: "root", | ||||
|  |                 path: '', | ||||
|  |                 component: Dashboard, | ||||
|  |                 children: [ | ||||
|  |                     { | ||||
|  |                         name: "DashboardHome", | ||||
|  |                         path: '/dashboard', | ||||
|  |                         component: DashboardHome, | ||||
|  |                         children: [ | ||||
|  |                             { | ||||
|  |                                 path: ':id', | ||||
|  |                                 component: Details, | ||||
|  |                             }, | ||||
|  |                             { | ||||
|  |                                 path: '/add', | ||||
|  |                                 component: EditMonitor, | ||||
|  |                             }, | ||||
|  |                             { | ||||
|  |                                 path: '/edit/:id', | ||||
|  |                                 component: EditMonitor, | ||||
|  |                             }, | ||||
|  |                         ] | ||||
|  |                     }, | ||||
|  |                     { | ||||
|  |                         path: '/settings', | ||||
|  |                         component: Settings, | ||||
|  |                     }, | ||||
|  |                 ], | ||||
|  |             }, | ||||
|  |         ], | ||||
|  |     } | ||||
|  | ] | ||||
|  | 
 | ||||
|  | const router = createRouter({ | ||||
|  |     linkActiveClass: 'active', | ||||
|  |     history: createWebHistory(), | ||||
|  |     routes, | ||||
|  | }) | ||||
|  | 
 | ||||
|  | const app = createApp({ | ||||
|  |     mixins: [ | ||||
|  |         socket, | ||||
|  |     ], | ||||
|  |     render: ()=>h(App) | ||||
|  | }) | ||||
|  | 
 | ||||
|  | app.use(router) | ||||
|  | 
 | ||||
|  | const options = { | ||||
|  |     position: "bottom-right" | ||||
|  | }; | ||||
|  | 
 | ||||
|  | app.use(Toast, options); | ||||
|  | 
 | ||||
|  | app.mount('#app') | ||||
|  | 
 | ||||
| @ -0,0 +1,121 @@ | |||||
|  | import {io} from "socket.io-client"; | ||||
|  | import { useToast } from 'vue-toastification' | ||||
|  | const toast = useToast() | ||||
|  | 
 | ||||
|  | let storage = localStorage; | ||||
|  | let socket; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  | 
 | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             socket: { | ||||
|  |                 token: null, | ||||
|  |                 firstConnect: true, | ||||
|  |                 connected: false, | ||||
|  |             }, | ||||
|  |             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: [ | ||||
|  | 
 | ||||
|  |             ] | ||||
|  |         } | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     created() { | ||||
|  |         socket = io("http://localhost:3001", { | ||||
|  |             transports: ['websocket'] | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on('monitorList', (data) => { | ||||
|  |             this.monitorList = data; | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on('disconnect', () => { | ||||
|  |             this.socket.connected = false; | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |         socket.on('connect', () => { | ||||
|  |             this.socket.connected = true; | ||||
|  |             this.socket.firstConnect = false; | ||||
|  | 
 | ||||
|  |             if (storage.token) { | ||||
|  |                 this.loginByToken(storage.token) | ||||
|  |             } else { | ||||
|  |                 this.allowLoginDialog = true; | ||||
|  |             } | ||||
|  | 
 | ||||
|  |         }); | ||||
|  | 
 | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     methods: { | ||||
|  |         getSocket() { | ||||
|  |           return socket; | ||||
|  |         }, | ||||
|  |         toastRes(res) { | ||||
|  |             if (res.ok) { | ||||
|  |                 toast.success(res.msg); | ||||
|  |             } else { | ||||
|  |                 toast.error(res.msg); | ||||
|  |             } | ||||
|  |         }, | ||||
|  |         login(username, password, callback) { | ||||
|  |             socket.emit("login", { | ||||
|  |                 username, | ||||
|  |                 password, | ||||
|  |             }, (res) => { | ||||
|  | 
 | ||||
|  |                 if (res.ok) { | ||||
|  |                     storage.token = res.token; | ||||
|  |                     this.socket.token = res.token; | ||||
|  |                     this.loggedIn = true; | ||||
|  | 
 | ||||
|  |                     // Trigger Chrome Save Password
 | ||||
|  |                     history.pushState({}, '') | ||||
|  |                 } | ||||
|  | 
 | ||||
|  |                 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; | ||||
|  |             this.loggedIn = false; | ||||
|  | 
 | ||||
|  |             socket.emit("logout", () => { | ||||
|  |                 toast.success("Logout Successfully") | ||||
|  |             }) | ||||
|  |         }, | ||||
|  |         add(monitor, callback) { | ||||
|  |             socket.emit("add", monitor, callback) | ||||
|  |         }, | ||||
|  |         deleteMonitor(monitorID, callback) { | ||||
|  |             socket.emit("deleteMonitor", monitorID, callback) | ||||
|  |         }, | ||||
|  |         loadMonitor(monitorID) { | ||||
|  | 
 | ||||
|  |         } | ||||
|  |     }, | ||||
|  | 
 | ||||
|  |     computed: { | ||||
|  | 
 | ||||
|  |     } | ||||
|  | 
 | ||||
|  | } | ||||
|  | 
 | ||||
| @ -0,0 +1,128 @@ | |||||
|  | <template> | ||||
|  | 
 | ||||
|  |     <div class="container-fluid"> | ||||
|  |         <div class="row"> | ||||
|  |             <div class="col-12 col-xl-4"> | ||||
|  |                 <div> | ||||
|  |                     <router-link to="/add" class="btn btn-primary">Add New Monitor</router-link> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div class="shadow-box list"> | ||||
|  | 
 | ||||
|  |                     <span v-if="$root.monitorList.length === 0">No Monitors, please <router-link to="/add">add one</router-link>.</span> | ||||
|  | 
 | ||||
|  |                     <router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in $root.monitorList"> | ||||
|  | 
 | ||||
|  |                         <div class="row"> | ||||
|  |                         	<div class="col-6"> | ||||
|  | 
 | ||||
|  |                                 <div class="info"> | ||||
|  |                                     <span class="badge rounded-pill bg-primary">{{ item.upRate }}%</span> | ||||
|  |                                     {{ item.name }} | ||||
|  |                                 </div> | ||||
|  | 
 | ||||
|  |                             </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> | ||||
|  |                             </div> | ||||
|  |                         </div> | ||||
|  | 
 | ||||
|  |                     </router-link> | ||||
|  | 
 | ||||
|  |                 </div> | ||||
|  |             </div> | ||||
|  |             <div class="col-12 col-xl-8"> | ||||
|  |                 <router-view /> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  |     }, | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         monitorURL(id) { | ||||
|  |             return "/dashboard/" + id; | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped lang="scss"> | ||||
|  | @import "../assets/vars.scss"; | ||||
|  | 
 | ||||
|  | .container-fluid { | ||||
|  |     width: 98% | ||||
|  | } | ||||
|  | 
 | ||||
|  | .list { | ||||
|  |     margin-top: 25px; | ||||
|  | 
 | ||||
|  |     .item { | ||||
|  |         display: block; | ||||
|  |         text-decoration: none; | ||||
|  |         padding: 15px 15px 12px 15px; | ||||
|  |         border-radius: 10px; | ||||
|  |         transition: all ease-in-out 0.15s; | ||||
|  | 
 | ||||
|  |         &.disabled { | ||||
|  |             opacity: 0.3; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         .info { | ||||
|  |             white-space: nowrap; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         &:hover { | ||||
|  |             background-color: $highlight-white; | ||||
|  |         } | ||||
|  | 
 | ||||
|  |         &.active { | ||||
|  |             background-color: #cdf8f4; | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .hp-bar { | ||||
|  |     white-space: nowrap; | ||||
|  |     margin-top: 4px; | ||||
|  |     text-align: right; | ||||
|  | 
 | ||||
|  |     div { | ||||
|  |         display: inline-block; | ||||
|  |         background-color: $primary; | ||||
|  |         width: 0.35rem; | ||||
|  |         height: 1rem; | ||||
|  |         margin: 0.15rem; | ||||
|  |         border-radius: 50rem; | ||||
|  |         transition: all ease-in-out 0.15s; | ||||
|  | 
 | ||||
|  |         &:hover { | ||||
|  |             opacity: 0.8; | ||||
|  |             transform: scale(1.5); | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | </style> | ||||
| @ -0,0 +1,123 @@ | |||||
|  | <template> | ||||
|  | 
 | ||||
|  |     <div v-if="$route.name === 'DashboardHome'"> | ||||
|  |         <h1 class="mb-3">Quick Stats</h1> | ||||
|  | 
 | ||||
|  |         <div class="shadow-box big-padding text-center"> | ||||
|  |             <div class="row"> | ||||
|  | 
 | ||||
|  |                 <div class="col-12"> | ||||
|  |                     <div class="hp-bar-big"> | ||||
|  |                         <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></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> | ||||
|  |                         <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></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                         <div></div> | ||||
|  |                     </div> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |             	<div class="col"> | ||||
|  |                     <h3>Up</h3> | ||||
|  |                     <span class="num">2</span> | ||||
|  |                 </div> | ||||
|  |             	<div class="col"> | ||||
|  |                     <h3>Down</h3> | ||||
|  |                     <span class="num text-danger">0</span> | ||||
|  |                 </div> | ||||
|  |                 <div class="col"> | ||||
|  |                     <h3>Pause</h3> | ||||
|  |                     <span class="num">0</span> | ||||
|  |                 </div> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <div class="row mt-4"> | ||||
|  |         	<div class="col-8"> | ||||
|  |                 <h4>Latest Incident</h4> | ||||
|  | 
 | ||||
|  |                 <div class="shadow-box bg-danger text-light"> | ||||
|  |                     MySQL was down. | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |                 <div class="shadow-box bg-primary text-light"> | ||||
|  |                     No issues was found. | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |             </div> | ||||
|  |         	<div class="col-4"> | ||||
|  | 
 | ||||
|  |                 <h4>Overall Uptime</h4> | ||||
|  | 
 | ||||
|  |                 <div class="shadow-box"> | ||||
|  |                     <div>100.00% (24 hours)</div> | ||||
|  |                     <div>100.00% (7 days)</div> | ||||
|  |                     <div>100.00% (30 days)</div> | ||||
|  |                 </div> | ||||
|  | 
 | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  |     <router-view ref="child" /> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | export default { | ||||
|  |     computed: { | ||||
|  | 
 | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped lang="scss"> | ||||
|  | @import "../assets/vars"; | ||||
|  | 
 | ||||
|  | .num { | ||||
|  |     font-size: 30px; | ||||
|  |     color: $primary; | ||||
|  |     font-weight: bold; | ||||
|  | } | ||||
|  | </style> | ||||
| @ -0,0 +1,162 @@ | |||||
|  | <template> | ||||
|  |     <h1>{{ monitor.name }}</h1> | ||||
|  |     <h2>{{ monitor.url }}</h2> | ||||
|  | 
 | ||||
|  |     <div class="functions"> | ||||
|  |         <button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> | ||||
|  |         <button class="btn btn-primary" @click="resumeMonitor"  v-if="! monitor.active">Resume</button> | ||||
|  |         <router-link :to=" '/edit/' + monitor.id " class="btn btn-light">Edit</router-link> | ||||
|  |         <button class="btn btn-danger" @click="deleteDialog">Delete</button> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  |     <div class="shadow-box"> | ||||
|  | 
 | ||||
|  |         <div class="hp-bar-big"> | ||||
|  |             <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></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> | ||||
|  |             <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></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |             <div></div> | ||||
|  |         </div> | ||||
|  | 
 | ||||
|  |         <div class="row"> | ||||
|  |             <div class="col-md-8"> | ||||
|  | 
 | ||||
|  |             </div> | ||||
|  |             <div class="col-md-4"> | ||||
|  | 
 | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  |     <Confirm ref="confirmPause" @yes="pauseMonitor"> | ||||
|  |         Are you sure want to pause? | ||||
|  |     </Confirm> | ||||
|  | 
 | ||||
|  |     <Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor"> | ||||
|  |         Are you sure want to delete this monitor? | ||||
|  |     </Confirm> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import { useToast } from 'vue-toastification' | ||||
|  | const toast = useToast() | ||||
|  | import Confirm from "../components/Confirm.vue"; | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  |         Confirm | ||||
|  |     }, | ||||
|  |     mounted() { | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  | 
 | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     computed: { | ||||
|  |         monitor() { | ||||
|  |             let id = parseInt(this.$route.params.id) | ||||
|  | 
 | ||||
|  |             for (let monitor of this.$root.monitorList) { | ||||
|  |                 if (monitor.id === id) { | ||||
|  |                     return monitor; | ||||
|  |                 } | ||||
|  |             } | ||||
|  |             return {}; | ||||
|  |         }, | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         pauseDialog() { | ||||
|  |             this.$refs.confirmPause.show(); | ||||
|  |         }, | ||||
|  |         resumeMonitor() { | ||||
|  |             this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => { | ||||
|  |                 this.$root.toastRes(res) | ||||
|  |             }) | ||||
|  |         }, | ||||
|  |         pauseMonitor() { | ||||
|  |             this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => { | ||||
|  |                 this.$root.toastRes(res) | ||||
|  |             }) | ||||
|  |         }, | ||||
|  |         deleteDialog() { | ||||
|  |             this.$refs.confirmDelete.show(); | ||||
|  |         }, | ||||
|  |         deleteMonitor() { | ||||
|  |             this.$root.deleteMonitor(this.monitor.id, (res) => { | ||||
|  |                 if (res.ok) { | ||||
|  |                     toast.success(res.msg); | ||||
|  |                     this.$router.push("/dashboard") | ||||
|  |                 } else { | ||||
|  |                     toast.error(res.msg); | ||||
|  |                 } | ||||
|  |             }) | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style lang="scss" scoped> | ||||
|  | @import "../assets/vars.scss"; | ||||
|  | 
 | ||||
|  | h2 { | ||||
|  |     color: $primary; | ||||
|  |     margin-bottom: 20px; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .functions { | ||||
|  |     button, a { | ||||
|  |         margin-right: 20px; | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | .shadow-box { | ||||
|  |     padding: 20px; | ||||
|  |     margin-top: 25px; | ||||
|  | } | ||||
|  | </style> | ||||
| @ -0,0 +1,123 @@ | |||||
|  | <template> | ||||
|  |     <h1 class="mb-3">{{ pageName }}</h1> | ||||
|  |     <form @submit.prevent="submit"> | ||||
|  | 
 | ||||
|  |     <div class="shadow-box"> | ||||
|  |         <div class="row"> | ||||
|  |             <div class="col-md-6"> | ||||
|  |                 <h2>General</h2> | ||||
|  | 
 | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="type" class="form-label">Monitor Type</label> | ||||
|  |                         <select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type"> | ||||
|  |                             <option value="http">HTTP(s)</option> | ||||
|  |                             <option value="port">TCP Port</option> | ||||
|  |                             <option value="ping">Ping</option> | ||||
|  |                             <option value="keyword">HTTP(s) - Keyword</option> | ||||
|  |                         </select> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="name" class="form-label">Friendly Name</label> | ||||
|  |                         <input type="text" class="form-control" id="name" v-model="monitor.name" required> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="url" class="form-label">URL</label> | ||||
|  |                         <input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <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"> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div> | ||||
|  |                         <button class="btn btn-primary" type="submit" :disabled="processing">Save</button> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <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> | ||||
|  |             </div> | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  |     </form> | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | import { useToast } from 'vue-toastification' | ||||
|  | const toast = useToast() | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     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) | ||||
|  |                 } | ||||
|  |             }) | ||||
|  |         } | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             processing: false, | ||||
|  |             monitor: { } | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     computed: { | ||||
|  |         pageName() { | ||||
|  |             return (this.isAdd) ? "Add New Monitor" : "Edit" | ||||
|  |         }, | ||||
|  |         isAdd() { | ||||
|  |             return this.$route.path === "/add"; | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         submit() { | ||||
|  |             this.processing = true; | ||||
|  | 
 | ||||
|  |             if (this.isAdd) { | ||||
|  |                 this.$root.add(this.monitor, (res) => { | ||||
|  |                     this.processing = false; | ||||
|  | 
 | ||||
|  |                     if (res.ok) { | ||||
|  |                         toast.success(res.msg); | ||||
|  |                         this.$router.push("/dashboard/" + res.monitorID) | ||||
|  |                     } else { | ||||
|  |                         toast.error(res.msg); | ||||
|  |                     } | ||||
|  | 
 | ||||
|  |                 }) | ||||
|  |             } else { | ||||
|  | 
 | ||||
|  |             } | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped> | ||||
|  |     .shadow-box { | ||||
|  |         padding: 20px; | ||||
|  |     } | ||||
|  | </style> | ||||
| @ -0,0 +1,111 @@ | |||||
|  | <template> | ||||
|  |     <h1 class="mb-3">Settings</h1> | ||||
|  | 
 | ||||
|  |     <div class="shadow-box"> | ||||
|  |         <div class="row"> | ||||
|  | 
 | ||||
|  |             <div class="col-md-6"> | ||||
|  |                 <h2>General</h2> | ||||
|  |                 <form class="mb-3"> | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="timezone" class="form-label">Timezone</label> | ||||
|  |                         <select class="form-select" aria-label="Default select example" id="timezone"> | ||||
|  |                             <option value="1">One</option> | ||||
|  |                             <option value="2">Two</option> | ||||
|  |                             <option value="3">Three</option> | ||||
|  |                         </select> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div> | ||||
|  |                         <button class="btn btn-primary" type="submit">Save</button> | ||||
|  |                     </div> | ||||
|  |                 </form> | ||||
|  | 
 | ||||
|  |                 <h2>Change Password</h2> | ||||
|  |                 <form class="mb-3" @submit.prevent="savePassword"> | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="current-password" class="form-label">Current Password</label> | ||||
|  |                         <input type="password" class="form-control" id="current-password" required v-model="password.currentPassword"> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="new-password" class="form-label">New Password</label> | ||||
|  |                         <input type="password" class="form-control" id="new-password" required v-model="password.newPassword"> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div class="mb-3"> | ||||
|  |                         <label for="repeat-new-password" class="form-label">Repeat New Password</label> | ||||
|  |                         <input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword"> | ||||
|  |                         <div class="invalid-feedback"> | ||||
|  |                             The repeat password is not match. | ||||
|  |                         </div> | ||||
|  |                     </div> | ||||
|  | 
 | ||||
|  |                     <div> | ||||
|  |                         <button class="btn btn-primary" type="submit">Update Password</button> | ||||
|  |                     </div> | ||||
|  |                 </form> | ||||
|  | 
 | ||||
|  |                 <div> | ||||
|  |                     <button class="btn btn-danger" @click="$root.logout">Logout</button> | ||||
|  |                 </div> | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |             <div class="col-md-6"> | ||||
|  |                 <h2>Notifications</h2> | ||||
|  |                 <p>Empty</p> | ||||
|  |                 <button class="btn btn-primary" type="submit">Add Notification</button> | ||||
|  |             </div> | ||||
|  | 
 | ||||
|  |         </div> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script> | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | export default { | ||||
|  |     components: { | ||||
|  | 
 | ||||
|  |     }, | ||||
|  |     data() { | ||||
|  |         return { | ||||
|  |             invalidPassword: false, | ||||
|  |             password: { | ||||
|  |                 currentPassword: "", | ||||
|  |                 newPassword: "", | ||||
|  |                 repeatNewPassword: "", | ||||
|  |             } | ||||
|  |         } | ||||
|  |     }, | ||||
|  |     methods: { | ||||
|  |         savePassword() { | ||||
|  |             if (this.password.newPassword !== this.password.repeatNewPassword) { | ||||
|  |                 this.invalidPassword = true; | ||||
|  |             } else { | ||||
|  |                 this.$root.getSocket().emit("changePassword", this.password, (res) => { | ||||
|  |                     this.$root.toastRes(res) | ||||
|  |                     if (res.ok) { | ||||
|  |                         this.password.currentPassword = "" | ||||
|  |                         this.password.newPassword = "" | ||||
|  |                         this.password.repeatNewPassword = "" | ||||
|  |                     } | ||||
|  |                 }) | ||||
|  |             } | ||||
|  |         }, | ||||
|  |     }, | ||||
|  |     watch: { | ||||
|  |         "password.repeatNewPassword"() { | ||||
|  |             this.invalidPassword = false; | ||||
|  |         } | ||||
|  |     } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style scoped> | ||||
|  |     .shadow-box { | ||||
|  |         padding: 20px; | ||||
|  |     } | ||||
|  | </style> | ||||
					Loading…
					
					
				
		Reference in new issue