diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/server/model/monitor.js b/server/model/monitor.js
new file mode 100644
index 00000000..1b25270f
--- /dev/null
+++ b/server/model/monitor.js
@@ -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;
diff --git a/server/server.js b/server/server.js
new file mode 100644
index 00000000..a3d10ddd
--- /dev/null
+++ b/server/server.js
@@ -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;
+    }
+}
+
diff --git a/server/util.js b/server/util.js
new file mode 100644
index 00000000..fe3ed4a0
--- /dev/null
+++ b/server/util.js
@@ -0,0 +1,3 @@
+exports.sleep = (ms) => {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 00000000..1f05560e
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,13 @@
+<template>
+    <router-view />
+</template>
+
+<script>
+export default {
+
+}
+</script>
+
+<style lang="scss">
+
+</style>
diff --git a/src/assets/app.scss b/src/assets/app.scss
new file mode 100644
index 00000000..a17e1884
--- /dev/null
+++ b/src/assets/app.scss
@@ -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);
+        }
+    }
+}
diff --git a/src/assets/vars.scss b/src/assets/vars.scss
new file mode 100644
index 00000000..dd1bcc0b
--- /dev/null
+++ b/src/assets/vars.scss
@@ -0,0 +1,6 @@
+$primary: #5CDD8B;
+$link-color: #111;
+$border-radius: 50rem;
+
+$highlight: #7ce8a4;
+$highlight-white: #e7faec;
diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue
new file mode 100644
index 00000000..063ece25
--- /dev/null
+++ b/src/components/Confirm.vue
@@ -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>
diff --git a/src/components/Login.vue b/src/components/Login.vue
new file mode 100644
index 00000000..5907f616
--- /dev/null
+++ b/src/components/Login.vue
@@ -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>
diff --git a/src/layouts/Layout.vue b/src/layouts/Layout.vue
new file mode 100644
index 00000000..9e3d7a65
--- /dev/null
+++ b/src/layouts/Layout.vue
@@ -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>
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..085c25f2
--- /dev/null
+++ b/src/main.js
@@ -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')
+
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
new file mode 100644
index 00000000..a55a1d3e
--- /dev/null
+++ b/src/mixins/socket.js
@@ -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: {
+
+    }
+
+}
+
diff --git a/src/pages/Dashboard.vue b/src/pages/Dashboard.vue
new file mode 100644
index 00000000..8da16248
--- /dev/null
+++ b/src/pages/Dashboard.vue
@@ -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>
diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue
new file mode 100644
index 00000000..b9ecfefb
--- /dev/null
+++ b/src/pages/DashboardHome.vue
@@ -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>
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
new file mode 100644
index 00000000..d1e04d1c
--- /dev/null
+++ b/src/pages/Details.vue
@@ -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>
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
new file mode 100644
index 00000000..143806a7
--- /dev/null
+++ b/src/pages/EditMonitor.vue
@@ -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>
diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue
new file mode 100644
index 00000000..77a4add1
--- /dev/null
+++ b/src/pages/Settings.vue
@@ -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>