mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	[new status page] wip
This commit is contained in:
		@@ -10,7 +10,9 @@ CREATE TABLE [status_page](
 | 
			
		||||
    [published] BOOLEAN NOT NULL DEFAULT 1,
 | 
			
		||||
    [search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
 | 
			
		||||
    [show_tags] BOOLEAN NOT NULL DEFAULT 0,
 | 
			
		||||
    [password] VARCHAR
 | 
			
		||||
    [password] VARCHAR,
 | 
			
		||||
    [date_created] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
			
		||||
    [date_modified] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								server/model/status_page.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/model/status_page.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
const { BeanModel } = require("redbean-node/dist/bean-model");
 | 
			
		||||
const { R } = require("redbean-node");
 | 
			
		||||
 | 
			
		||||
class StatusPage extends BeanModel {
 | 
			
		||||
 | 
			
		||||
    static async sendStatusPageList(io, socket) {
 | 
			
		||||
        let result = {};
 | 
			
		||||
 | 
			
		||||
        let list = await R.findAll("status_page", " ORDER BY title ");
 | 
			
		||||
 | 
			
		||||
        for (let item of list) {
 | 
			
		||||
            result[item.id] = await item.toJSON();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        io.to(socket.userID).emit("statusPageList", result);
 | 
			
		||||
        return list;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async toJSON() {
 | 
			
		||||
        return {
 | 
			
		||||
            id: this.id,
 | 
			
		||||
            slug: this.slug,
 | 
			
		||||
            title: this.title,
 | 
			
		||||
            icon: this.icon,
 | 
			
		||||
            theme: this.theme,
 | 
			
		||||
            published: !!this.published,
 | 
			
		||||
            showTags: !!this.show_tags,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async toPublicJSON() {
 | 
			
		||||
        return {
 | 
			
		||||
            slug: this.slug,
 | 
			
		||||
            title: this.title,
 | 
			
		||||
            icon: this.icon,
 | 
			
		||||
            theme: this.theme,
 | 
			
		||||
            published: !!this.published,
 | 
			
		||||
            showTags: !!this.show_tags,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = StatusPage;
 | 
			
		||||
@@ -83,33 +83,28 @@ router.get("/api/push/:pushToken", async (request, response) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Status Page Config
 | 
			
		||||
router.get("/api/status-page/config", async (_request, response) => {
 | 
			
		||||
router.get("/api/status-page/config/:slug", async (request, response) => {
 | 
			
		||||
    allowDevAllOrigin(response);
 | 
			
		||||
    let slug = request.params.slug;
 | 
			
		||||
 | 
			
		||||
    let config = await getSettings("statusPage");
 | 
			
		||||
    let statusPage = await R.findOne("status_page", " slug = ? ", [
 | 
			
		||||
        slug
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    if (! config.statusPageTheme) {
 | 
			
		||||
        config.statusPageTheme = "light";
 | 
			
		||||
    if (!statusPage) {
 | 
			
		||||
        response.statusCode = 404;
 | 
			
		||||
        response.json({
 | 
			
		||||
            msg: "Not Found"
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (! config.statusPagePublished) {
 | 
			
		||||
        config.statusPagePublished = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (! config.statusPageTags) {
 | 
			
		||||
        config.statusPageTags = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (! config.title) {
 | 
			
		||||
        config.title = "Uptime Kuma";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.json(config);
 | 
			
		||||
    response.json(await statusPage.toPublicJSON());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Status Page - Get the current Incident
 | 
			
		||||
// Can fetch only if published
 | 
			
		||||
router.get("/api/status-page/incident", async (_, response) => {
 | 
			
		||||
router.get("/api/status-page/incident/:slug", async (_, response) => {
 | 
			
		||||
    allowDevAllOrigin(response);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
@@ -133,7 +128,7 @@ router.get("/api/status-page/incident", async (_, response) => {
 | 
			
		||||
 | 
			
		||||
// Status Page - Monitor List
 | 
			
		||||
// Can fetch only if published
 | 
			
		||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
 | 
			
		||||
router.get("/api/status-page/monitor-list/:slug", cache("5 minutes"), async (_request, response) => {
 | 
			
		||||
    allowDevAllOrigin(response);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
@@ -172,7 +167,7 @@ router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request,
 | 
			
		||||
 | 
			
		||||
// Status Page Polling Data
 | 
			
		||||
// Can fetch only if published
 | 
			
		||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
 | 
			
		||||
router.get("/api/status-page/heartbeat/:slug", cache("5 minutes"), async (_request, response) => {
 | 
			
		||||
    allowDevAllOrigin(response);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
 | 
			
		||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
 | 
			
		||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
 | 
			
		||||
const TwoFA = require("./2fa");
 | 
			
		||||
const StatusPage = require("./model/status_page");
 | 
			
		||||
 | 
			
		||||
app.use(express.json());
 | 
			
		||||
 | 
			
		||||
@@ -1414,6 +1415,8 @@ async function afterLogin(socket, user) {
 | 
			
		||||
    for (let monitorID in monitorList) {
 | 
			
		||||
        await Monitor.sendStats(io, monitorID, user.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await StatusPage.sendStatusPageList(io, socket);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getMonitorJSONList(userID) {
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,10 @@ textarea.form-control {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.btn-dark {
 | 
			
		||||
    background-color: #161B22;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 550px) {
 | 
			
		||||
    .table-shadow-box {
 | 
			
		||||
        padding: 10px !important;
 | 
			
		||||
@@ -167,7 +171,7 @@ textarea.form-control {
 | 
			
		||||
        background-color: #232f3b;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a,
 | 
			
		||||
    a:not(.btn),
 | 
			
		||||
    .table,
 | 
			
		||||
    .nav-link {
 | 
			
		||||
        color: $dark-font-color;
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,8 @@ import {
 | 
			
		||||
    faAward,
 | 
			
		||||
    faLink,
 | 
			
		||||
    faChevronDown,
 | 
			
		||||
    faPen,
 | 
			
		||||
    faExternalLinkSquareAlt,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
 | 
			
		||||
library.add(
 | 
			
		||||
@@ -67,6 +69,8 @@ library.add(
 | 
			
		||||
    faAward,
 | 
			
		||||
    faLink,
 | 
			
		||||
    faChevronDown,
 | 
			
		||||
    faPen,
 | 
			
		||||
    faExternalLinkSquareAlt,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export { FontAwesomeIcon };
 | 
			
		||||
 
 | 
			
		||||
@@ -183,7 +183,7 @@ export default {
 | 
			
		||||
    "Edit Status Page": "Edit Status Page",
 | 
			
		||||
    "Go to Dashboard": "Go to Dashboard",
 | 
			
		||||
    "Status Page": "Status Page",
 | 
			
		||||
    "Status Pages": "Status Page",
 | 
			
		||||
    "Status Pages": "Status Pages",
 | 
			
		||||
    defaultNotificationName: "My {notification} Alert ({number})",
 | 
			
		||||
    here: "here",
 | 
			
		||||
    Required: "Required",
 | 
			
		||||
 
 | 
			
		||||
@@ -19,9 +19,9 @@
 | 
			
		||||
 | 
			
		||||
            <ul class="nav nav-pills">
 | 
			
		||||
                <li class="nav-item me-2">
 | 
			
		||||
                    <a href="/status" class="nav-link status-page">
 | 
			
		||||
                        <font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <router-link to="/manage-status-page" class="nav-link">
 | 
			
		||||
                        <font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
 | 
			
		||||
                    </router-link>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li v-if="$root.loggedIn" class="nav-item me-2">
 | 
			
		||||
                    <router-link to="/dashboard" class="nav-link">
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ export default {
 | 
			
		||||
            uptimeList: { },
 | 
			
		||||
            tlsInfoList: {},
 | 
			
		||||
            notificationList: [],
 | 
			
		||||
            statusPageList: [],
 | 
			
		||||
            connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
@@ -103,6 +104,11 @@ export default {
 | 
			
		||||
                this.notificationList = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("statusPageList", (data) => {
 | 
			
		||||
                console.log(data);
 | 
			
		||||
                this.statusPageList = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("heartbeat", (data) => {
 | 
			
		||||
                if (! (data.monitorID in this.heartbeatList)) {
 | 
			
		||||
                    this.heartbeatList[data.monitorID] = [];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										104
									
								
								src/pages/ManageStatusPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/pages/ManageStatusPage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <transition name="slide-fade" appear>
 | 
			
		||||
        <div>
 | 
			
		||||
            <h1 class="mb-3">
 | 
			
		||||
                {{ $t("Status Pages") }}
 | 
			
		||||
            </h1>
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
                <router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Status Page") }}</router-link>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="shadow-box">
 | 
			
		||||
                <div v-for="statusPage in $root.statusPageList" class="item">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div class="col">
 | 
			
		||||
                            <div class="title">{{ statusPage.title }}</div>
 | 
			
		||||
                            <div class="slug">/status/{{ statusPage.slug }}</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-lg-6 col-xl-5">
 | 
			
		||||
                            <div class="btn-group">
 | 
			
		||||
                                <a target="_blank" :href="'/status/' + statusPage.slug" class="btn btn-dark">
 | 
			
		||||
                                    <font-awesome-icon icon="external-link-square-alt" /><br />
 | 
			
		||||
                                    {{ $t("Manage") }}
 | 
			
		||||
                                </a>
 | 
			
		||||
 | 
			
		||||
                                <router-link to="/" class="btn btn-danger">
 | 
			
		||||
                                    <font-awesome-icon icon="trash" /><br />
 | 
			
		||||
                                    {{ $t("Delete") }}
 | 
			
		||||
                                </router-link>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
    @import "../assets/vars.scss";
 | 
			
		||||
 | 
			
		||||
    .item {
 | 
			
		||||
        display: block;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
        padding: 13px 15px 10px 15px;
 | 
			
		||||
        border-radius: 10px;
 | 
			
		||||
        transition: all ease-in-out 0.15s;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
            background-color: $highlight-white;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.active {
 | 
			
		||||
            background-color: #cdf8f4;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .title {
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
            font-size: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .slug {
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .btn-group {
 | 
			
		||||
            //margin-top: 7px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .dark {
 | 
			
		||||
        .item {
 | 
			
		||||
            &:hover {
 | 
			
		||||
                background-color: $dark-bg2;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &.active {
 | 
			
		||||
                background-color: $dark-bg2;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -167,6 +167,8 @@ footer {
 | 
			
		||||
        margin: 0.5em;
 | 
			
		||||
        padding: 0.7em 1em;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        border-left-width: 0;
 | 
			
		||||
        transition: all ease-in-out 0.1s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .menu-item:hover {
 | 
			
		||||
 
 | 
			
		||||
@@ -247,6 +247,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            slug: null,
 | 
			
		||||
            enableEditMode: false,
 | 
			
		||||
            enableEditIncidentMode: false,
 | 
			
		||||
            hasToken: false,
 | 
			
		||||
@@ -296,15 +297,15 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        isPublished() {
 | 
			
		||||
            return this.config.statusPagePublished;
 | 
			
		||||
            return this.config.published;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        theme() {
 | 
			
		||||
            return this.config.statusPageTheme;
 | 
			
		||||
            return this.config.theme;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        tagsVisible() {
 | 
			
		||||
            return this.config.statusPageTags
 | 
			
		||||
            return this.config.showTags;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        logoClass() {
 | 
			
		||||
@@ -378,8 +379,8 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Set Theme
 | 
			
		||||
        "config.statusPageTheme"() {
 | 
			
		||||
            this.$root.statusPageTheme = this.config.statusPageTheme;
 | 
			
		||||
        "config.theme"() {
 | 
			
		||||
            this.$root.statusPageTheme = this.config.theme;
 | 
			
		||||
            this.loadedTheme = true;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -409,7 +410,13 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    async mounted() {
 | 
			
		||||
        axios.get("/api/status-page/config").then((res) => {
 | 
			
		||||
        this.slug = this.$route.params.slug;
 | 
			
		||||
 | 
			
		||||
        if (!this.slug) {
 | 
			
		||||
            this.slug = "default";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        axios.get("/api/status-page/config/" + this.slug).then((res) => {
 | 
			
		||||
            this.config = res.data;
 | 
			
		||||
 | 
			
		||||
            if (this.config.logo) {
 | 
			
		||||
@@ -417,13 +424,13 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        axios.get("/api/status-page/incident").then((res) => {
 | 
			
		||||
        axios.get("/api/status-page/incident/" + this.slug).then((res) => {
 | 
			
		||||
            if (res.data.ok) {
 | 
			
		||||
                this.incident = res.data.incident;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        axios.get("/api/status-page/monitor-list").then((res) => {
 | 
			
		||||
        axios.get("/api/status-page/monitor-list/" + this.slug).then((res) => {
 | 
			
		||||
            this.$root.publicGroupList = res.data;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -438,7 +445,7 @@ export default {
 | 
			
		||||
        updateHeartbeatList() {
 | 
			
		||||
            // If editMode, it will use the data from websocket.
 | 
			
		||||
            if (! this.editMode) {
 | 
			
		||||
                axios.get("/api/status-page/heartbeat").then((res) => {
 | 
			
		||||
                axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
 | 
			
		||||
                    this.$root.heartbeatList = res.data.heartbeatList;
 | 
			
		||||
                    this.$root.uptimeList = res.data.uptimeList;
 | 
			
		||||
                    this.loadedData = true;
 | 
			
		||||
@@ -485,10 +492,10 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        changeTheme(name) {
 | 
			
		||||
            this.config.statusPageTheme = name;
 | 
			
		||||
            this.config.theme = name;
 | 
			
		||||
        },
 | 
			
		||||
        changeTagsVisibilty(newState) {
 | 
			
		||||
            this.config.statusPageTags = newState;
 | 
			
		||||
            this.config.showTags = newState;
 | 
			
		||||
 | 
			
		||||
            // On load, the status page will not include tags if it's not enabled for security reasons
 | 
			
		||||
            // Which means if we enable tags, it won't show in the UI until saved
 | 
			
		||||
@@ -501,9 +508,9 @@ export default {
 | 
			
		||||
                        return {
 | 
			
		||||
                            ...monitor,
 | 
			
		||||
                            tags: newState ? this.$root.monitorList[monitor.id].tags : []
 | 
			
		||||
                        }
 | 
			
		||||
                        };
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
 | 
			
		||||
import Security from "./components/settings/Security.vue";
 | 
			
		||||
import Backup from "./components/settings/Backup.vue";
 | 
			
		||||
import About from "./components/settings/About.vue";
 | 
			
		||||
import ManageStatusPage from "./pages/ManageStatusPage.vue";
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
    {
 | 
			
		||||
@@ -98,6 +99,10 @@ const routes = [
 | 
			
		||||
                            },
 | 
			
		||||
                        ]
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        path: "/manage-status-page",
 | 
			
		||||
                        component: ManageStatusPage,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
@@ -114,6 +119,10 @@ const routes = [
 | 
			
		||||
        path: "/status",
 | 
			
		||||
        component: StatusPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: "/status/:slug",
 | 
			
		||||
        component: StatusPage,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const router = createRouter({
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user