mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	Feat: Use UptimeCalculator for PingChart (#4264)
				
					
				
			Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
		@@ -149,6 +149,7 @@ const apicache = require("./modules/apicache");
 | 
				
			|||||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
 | 
					const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
 | 
				
			||||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
 | 
					const { EmbeddedMariaDB } = require("./embedded-mariadb");
 | 
				
			||||||
const { SetupDatabase } = require("./setup-database");
 | 
					const { SetupDatabase } = require("./setup-database");
 | 
				
			||||||
 | 
					const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.use(express.json());
 | 
					app.use(express.json());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1528,6 +1529,7 @@ let needSetup = false;
 | 
				
			|||||||
        apiKeySocketHandler(socket);
 | 
					        apiKeySocketHandler(socket);
 | 
				
			||||||
        remoteBrowserSocketHandler(socket);
 | 
					        remoteBrowserSocketHandler(socket);
 | 
				
			||||||
        generalSocketHandler(socket, server);
 | 
					        generalSocketHandler(socket, server);
 | 
				
			||||||
 | 
					        chartSocketHandler(socket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.debug("server", "added all socket handlers");
 | 
					        log.debug("server", "added all socket handlers");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								server/socket-handlers/chart-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								server/socket-handlers/chart-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					const { checkLogin } = require("../util-server");
 | 
				
			||||||
 | 
					const { UptimeCalculator } = require("../uptime-calculator");
 | 
				
			||||||
 | 
					const { log } = require("../../src/util");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.chartSocketHandler = (socket) => {
 | 
				
			||||||
 | 
					    socket.on("getMonitorChartData", async (monitorID, period, callback) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            checkLogin(socket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            log.debug("monitor", `Get Monitor Chart Data: ${monitorID} User ID: ${socket.userID}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (period == null) {
 | 
				
			||||||
 | 
					                throw new Error("Invalid period.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let data;
 | 
				
			||||||
 | 
					            if (period <= 24) {
 | 
				
			||||||
 | 
					                data = uptimeCalculator.getDataArray(period * 60, "minute");
 | 
				
			||||||
 | 
					            } else if (period <= 720) {
 | 
				
			||||||
 | 
					                data = uptimeCalculator.getDataArray(period, "hour");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                data = uptimeCalculator.getDataArray(period / 24, "day");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            callback({
 | 
				
			||||||
 | 
					                ok: true,
 | 
				
			||||||
 | 
					                data,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            callback({
 | 
				
			||||||
 | 
					                ok: false,
 | 
				
			||||||
 | 
					                msg: e.message,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -290,7 +290,7 @@ class UptimeCalculator {
 | 
				
			|||||||
        dailyStatBean.pingMax = dailyData.maxPing;
 | 
					        dailyStatBean.pingMax = dailyData.maxPing;
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // eslint-disable-next-line no-unused-vars
 | 
					            // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
            const { up, down, avgPing, minPing, maxPing, ...extras } = dailyData;
 | 
					            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
 | 
				
			||||||
            if (Object.keys(extras).length > 0) {
 | 
					            if (Object.keys(extras).length > 0) {
 | 
				
			||||||
                dailyStatBean.extras = JSON.stringify(extras);
 | 
					                dailyStatBean.extras = JSON.stringify(extras);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -305,7 +305,7 @@ class UptimeCalculator {
 | 
				
			|||||||
        hourlyStatBean.pingMax = hourlyData.maxPing;
 | 
					        hourlyStatBean.pingMax = hourlyData.maxPing;
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // eslint-disable-next-line no-unused-vars
 | 
					            // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
            const { up, down, avgPing, minPing, maxPing, ...extras } = hourlyData;
 | 
					            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
 | 
				
			||||||
            if (Object.keys(extras).length > 0) {
 | 
					            if (Object.keys(extras).length > 0) {
 | 
				
			||||||
                hourlyStatBean.extras = JSON.stringify(extras);
 | 
					                hourlyStatBean.extras = JSON.stringify(extras);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -320,7 +320,7 @@ class UptimeCalculator {
 | 
				
			|||||||
        minutelyStatBean.pingMax = minutelyData.maxPing;
 | 
					        minutelyStatBean.pingMax = minutelyData.maxPing;
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // eslint-disable-next-line no-unused-vars
 | 
					            // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
            const { up, down, avgPing, minPing, maxPing, ...extras } = minutelyData;
 | 
					            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
 | 
				
			||||||
            if (Object.keys(extras).length > 0) {
 | 
					            if (Object.keys(extras).length > 0) {
 | 
				
			||||||
                minutelyStatBean.extras = JSON.stringify(extras);
 | 
					                minutelyStatBean.extras = JSON.stringify(extras);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,24 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <div class="period-options">
 | 
					        <div class="period-options">
 | 
				
			||||||
            <button type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					            <button
 | 
				
			||||||
 | 
					                type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown"
 | 
				
			||||||
 | 
					                aria-expanded="false"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
                {{ chartPeriodOptions[chartPeriodHrs] }} 
 | 
					                {{ chartPeriodOptions[chartPeriodHrs] }} 
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
            <ul class="dropdown-menu dropdown-menu-end">
 | 
					            <ul class="dropdown-menu dropdown-menu-end">
 | 
				
			||||||
                <li v-for="(item, key) in chartPeriodOptions" :key="key">
 | 
					                <li v-for="(item, key) in chartPeriodOptions" :key="key">
 | 
				
			||||||
                    <a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
 | 
					                    <button
 | 
				
			||||||
 | 
					                        type="button" class="dropdown-item" :class="{ active: chartPeriodHrs == key }"
 | 
				
			||||||
 | 
					                        @click="chartPeriodHrs = key"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        {{ item }}
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
                </li>
 | 
					                </li>
 | 
				
			||||||
            </ul>
 | 
					            </ul>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="chart-wrapper" :class="{ loading : loading}">
 | 
					        <div class="chart-wrapper" :class="{ loading: loading }">
 | 
				
			||||||
            <Line :data="chartData" :options="chartOptions" />
 | 
					            <Line :data="chartData" :options="chartOptions" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -19,9 +27,8 @@
 | 
				
			|||||||
<script lang="js">
 | 
					<script lang="js">
 | 
				
			||||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
 | 
					import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
 | 
				
			||||||
import "chartjs-adapter-dayjs-4";
 | 
					import "chartjs-adapter-dayjs-4";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					 | 
				
			||||||
import { Line } from "vue-chartjs";
 | 
					import { Line } from "vue-chartjs";
 | 
				
			||||||
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
 | 
					import { UP, DOWN, PENDING, MAINTENANCE } from "../util.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
 | 
					Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,8 +46,9 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            loading: false,
 | 
					            loading: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Configurable filtering on top of the returned data
 | 
					            // Time period for the chart to display, in hours
 | 
				
			||||||
            chartPeriodHrs: 0,
 | 
					            // Initial value is 0 as a workaround for triggering a data fetch on created()
 | 
				
			||||||
 | 
					            chartPeriodHrs: "0",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            chartPeriodOptions: {
 | 
					            chartPeriodOptions: {
 | 
				
			||||||
                0: this.$t("recent"),
 | 
					                0: this.$t("recent"),
 | 
				
			||||||
@@ -50,9 +58,8 @@ export default {
 | 
				
			|||||||
                168: "1w",
 | 
					                168: "1w",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // A heartbeatList for 3h, 6h, 24h, 1w
 | 
					            chartRawData: null,
 | 
				
			||||||
            // Uses the $root.heartbeatList when value is null
 | 
					            chartDataFetchInterval: null,
 | 
				
			||||||
            heartbeatList: null
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
@@ -157,34 +164,197 @@ export default {
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        chartData() {
 | 
					        chartData() {
 | 
				
			||||||
 | 
					            if (this.chartPeriodHrs === "0") {
 | 
				
			||||||
 | 
					                return this.getChartDatapointsFromHeartbeatList();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                return this.getChartDatapointsFromStats();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    watch: {
 | 
				
			||||||
 | 
					        // Update chart data when the selected chart period changes
 | 
				
			||||||
 | 
					        chartPeriodHrs: function (newPeriod) {
 | 
				
			||||||
 | 
					            if (this.chartDataFetchInterval) {
 | 
				
			||||||
 | 
					                clearInterval(this.chartDataFetchInterval);
 | 
				
			||||||
 | 
					                this.chartDataFetchInterval = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // eslint-disable-next-line eqeqeq
 | 
				
			||||||
 | 
					            if (newPeriod == "0") {
 | 
				
			||||||
 | 
					                this.heartbeatList = null;
 | 
				
			||||||
 | 
					                this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.loading = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let period;
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    period = parseInt(newPeriod);
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                    // Invalid period
 | 
				
			||||||
 | 
					                    period = 24;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.$root.getMonitorChartData(this.monitorId, period, (res) => {
 | 
				
			||||||
 | 
					                    if (!res.ok) {
 | 
				
			||||||
 | 
					                        this.$root.toastError(res.msg);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        this.chartRawData = res.data;
 | 
				
			||||||
 | 
					                        this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    this.loading = false;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.chartDataFetchInterval = setInterval(() => {
 | 
				
			||||||
 | 
					                    this.$root.getMonitorChartData(this.monitorId, period, (res) => {
 | 
				
			||||||
 | 
					                        if (res.ok) {
 | 
				
			||||||
 | 
					                            this.chartRawData = res.data;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }, 5 * 60 * 1000);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    created() {
 | 
				
			||||||
 | 
					        // Load chart period from storage if saved
 | 
				
			||||||
 | 
					        let period = this.$root.storage()[`chart-period-${this.monitorId}`];
 | 
				
			||||||
 | 
					        if (period != null) {
 | 
				
			||||||
 | 
					            // Has this ever been not a string?
 | 
				
			||||||
 | 
					            if (typeof period !== "string") {
 | 
				
			||||||
 | 
					                period = period.toString();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.chartPeriodHrs = period;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.chartPeriodHrs = "24";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    beforeUnmount() {
 | 
				
			||||||
 | 
					        if (this.chartDataFetchInterval) {
 | 
				
			||||||
 | 
					            clearInterval(this.chartDataFetchInterval);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    methods: {
 | 
				
			||||||
 | 
					        // Get color of bar chart for this datapoint
 | 
				
			||||||
 | 
					        getBarColorForDatapoint(datapoint) {
 | 
				
			||||||
 | 
					            if (datapoint.maintenance != null) {
 | 
				
			||||||
 | 
					                // Target is in maintenance
 | 
				
			||||||
 | 
					                return "rgba(23,71,245,0.41)";
 | 
				
			||||||
 | 
					            } else if (datapoint.down === 0) {
 | 
				
			||||||
 | 
					                // Target is up, no need to display a bar
 | 
				
			||||||
 | 
					                return "#000";
 | 
				
			||||||
 | 
					            } else if (datapoint.up === 0) {
 | 
				
			||||||
 | 
					                // Target is down
 | 
				
			||||||
 | 
					                return "rgba(220, 53, 69, 0.41)";
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Show yellow for mixed status
 | 
				
			||||||
 | 
					                return "rgba(245, 182, 23, 0.41)";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // push datapoint to chartData
 | 
				
			||||||
 | 
					        pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData) {
 | 
				
			||||||
 | 
					            const x = this.$root.unixToDateTime(datapoint.timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Show ping values if it was up in this period
 | 
				
			||||||
 | 
					            avgPingData.push({
 | 
				
			||||||
 | 
					                x,
 | 
				
			||||||
 | 
					                y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.avgPing : null,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            minPingData.push({
 | 
				
			||||||
 | 
					                x,
 | 
				
			||||||
 | 
					                y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.minPing : null,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            maxPingData.push({
 | 
				
			||||||
 | 
					                x,
 | 
				
			||||||
 | 
					                y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.maxPing : null,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            downData.push({
 | 
				
			||||||
 | 
					                x,
 | 
				
			||||||
 | 
					                y: datapoint.down + (datapoint.maintenance || 0),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            colorData.push(this.getBarColorForDatapoint(datapoint));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // get the average of a set of datapoints
 | 
				
			||||||
 | 
					        getAverage(datapoints) {
 | 
				
			||||||
 | 
					            const totalUp = datapoints.reduce((total, current) => total + current.up, 0);
 | 
				
			||||||
 | 
					            const totalDown = datapoints.reduce((total, current) => total + current.down, 0);
 | 
				
			||||||
 | 
					            const totalMaintenance = datapoints.reduce((total, current) => total + (current.maintenance || 0), 0);
 | 
				
			||||||
 | 
					            const totalPing = datapoints.reduce((total, current) => total + current.avgPing * current.up, 0);
 | 
				
			||||||
 | 
					            const minPing = datapoints.reduce((min, current) => Math.min(min, current.minPing), Infinity);
 | 
				
			||||||
 | 
					            const maxPing = datapoints.reduce((max, current) => Math.max(max, current.maxPing), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Find the middle timestamp to use
 | 
				
			||||||
 | 
					            let midpoint = Math.floor(datapoints.length / 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                timestamp: datapoints[midpoint].timestamp,
 | 
				
			||||||
 | 
					                up: totalUp,
 | 
				
			||||||
 | 
					                down: totalDown,
 | 
				
			||||||
 | 
					                maintenance: totalMaintenance > 0 ? totalMaintenance : undefined,
 | 
				
			||||||
 | 
					                avgPing: totalUp > 0 ? totalPing / totalUp : 0,
 | 
				
			||||||
 | 
					                minPing,
 | 
				
			||||||
 | 
					                maxPing,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        getChartDatapointsFromHeartbeatList() {
 | 
				
			||||||
 | 
					            // Render chart using heartbeatList
 | 
				
			||||||
 | 
					            let lastHeartbeatTime;
 | 
				
			||||||
 | 
					            const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
 | 
				
			||||||
            let pingData = [];  // Ping Data for Line Chart, y-axis contains ping time
 | 
					            let pingData = [];  // Ping Data for Line Chart, y-axis contains ping time
 | 
				
			||||||
            let downData = [];  // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
 | 
					            let downData = [];  // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
 | 
				
			||||||
            let colorData = []; // Color Data for Bar Chart
 | 
					            let colorData = []; // Color Data for Bar Chart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let heartbeatList = this.heartbeatList ||
 | 
					            let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || [];
 | 
				
			||||||
             (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
 | 
					 | 
				
			||||||
             [];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            heartbeatList
 | 
					            for (const beat of heartbeatList) {
 | 
				
			||||||
                .filter(
 | 
					                const beatTime = this.$root.toDayjs(beat.time);
 | 
				
			||||||
                    // Filtering as data gets appended
 | 
					                const x = beatTime.format("YYYY-MM-DD HH:mm:ss");
 | 
				
			||||||
                    // not the most efficient, but works for now
 | 
					
 | 
				
			||||||
                    (beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(
 | 
					                // Insert empty datapoint to separate big gaps
 | 
				
			||||||
                        dayjs().subtract(Math.max(this.chartPeriodHrs, 6), "hours")
 | 
					                if (lastHeartbeatTime && monitorInterval) {
 | 
				
			||||||
                    )
 | 
					                    const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
 | 
				
			||||||
                )
 | 
					                    if (diff > monitorInterval * 1000 * 10) {
 | 
				
			||||||
                .map((beat) => {
 | 
					                        // Big gap detected
 | 
				
			||||||
                    const x = this.$root.datetime(beat.time);
 | 
					                        const gapX = [
 | 
				
			||||||
                    pingData.push({
 | 
					                            lastHeartbeatTime.add(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
 | 
				
			||||||
                        x,
 | 
					                            beatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss")
 | 
				
			||||||
                        y: beat.ping,
 | 
					                        ];
 | 
				
			||||||
                    });
 | 
					
 | 
				
			||||||
                    downData.push({
 | 
					                        for (const x of gapX) {
 | 
				
			||||||
                        x,
 | 
					                            pingData.push({
 | 
				
			||||||
                        y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
 | 
					                                x,
 | 
				
			||||||
                    });
 | 
					                                y: null,
 | 
				
			||||||
                    colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
 | 
					                            });
 | 
				
			||||||
 | 
					                            downData.push({
 | 
				
			||||||
 | 
					                                x,
 | 
				
			||||||
 | 
					                                y: null,
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                            colorData.push("#000");
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                pingData.push({
 | 
				
			||||||
 | 
					                    x,
 | 
				
			||||||
 | 
					                    y: beat.status === UP ? beat.ping : null,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					                downData.push({
 | 
				
			||||||
 | 
					                    x,
 | 
				
			||||||
 | 
					                    y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                switch (beat.status) {
 | 
				
			||||||
 | 
					                    case MAINTENANCE:
 | 
				
			||||||
 | 
					                        colorData.push("rgba(23 ,71, 245, 0.41)");
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    case PENDING:
 | 
				
			||||||
 | 
					                        colorData.push("rgba(245, 182, 23, 0.41)");
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    default:
 | 
				
			||||||
 | 
					                        colorData.push("rgba(220, 53, 69, 0.41)");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                lastHeartbeatTime = beatTime;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                datasets: [
 | 
					                datasets: [
 | 
				
			||||||
@@ -214,54 +384,155 @@ export default {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					        getChartDatapointsFromStats() {
 | 
				
			||||||
    watch: {
 | 
					            // Render chart using UptimeCalculator data
 | 
				
			||||||
        // Update chart data when the selected chart period changes
 | 
					            let lastHeartbeatTime;
 | 
				
			||||||
        chartPeriodHrs: function (newPeriod) {
 | 
					            const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // eslint-disable-next-line eqeqeq
 | 
					            let avgPingData = [];  // Ping Data for Line Chart, y-axis contains ping time
 | 
				
			||||||
            if (newPeriod == "0") {
 | 
					            let minPingData = [];  // Ping Data for Line Chart, y-axis contains ping time
 | 
				
			||||||
                this.heartbeatList = null;
 | 
					            let maxPingData = [];  // Ping Data for Line Chart, y-axis contains ping time
 | 
				
			||||||
                this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
 | 
					            let downData = [];  // Down Data for Bar Chart, y-axis is number of down datapoints in this period
 | 
				
			||||||
            } else {
 | 
					            let colorData = []; // Color Data for Bar Chart
 | 
				
			||||||
                this.loading = true;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
 | 
					            const period = parseInt(this.chartPeriodHrs);
 | 
				
			||||||
                    if (!res.ok) {
 | 
					            let aggregatePoints = period > 6 ? 12 : 4;
 | 
				
			||||||
                        this.$root.toastError(res.msg);
 | 
					
 | 
				
			||||||
 | 
					            let aggregateBuffer = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.chartRawData) {
 | 
				
			||||||
 | 
					                for (const datapoint of this.chartRawData) {
 | 
				
			||||||
 | 
					                    // Empty datapoints are ignored
 | 
				
			||||||
 | 
					                    if (datapoint.up === 0 && datapoint.down === 0 && datapoint.maintenance === 0) {
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const beatTime = this.$root.unixToDayjs(datapoint.timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Insert empty datapoint to separate big gaps
 | 
				
			||||||
 | 
					                    if (lastHeartbeatTime && monitorInterval) {
 | 
				
			||||||
 | 
					                        const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
 | 
				
			||||||
 | 
					                        const oneSecond = 1000;
 | 
				
			||||||
 | 
					                        const oneMinute = oneSecond * 60;
 | 
				
			||||||
 | 
					                        const oneHour = oneMinute * 60;
 | 
				
			||||||
 | 
					                        if ((period <= 24 && diff > Math.max(oneMinute * 10, monitorInterval * oneSecond * 10)) ||
 | 
				
			||||||
 | 
					                            (period > 24 && diff > Math.max(oneHour * 10, monitorInterval * oneSecond * 10))) {
 | 
				
			||||||
 | 
					                            // Big gap detected
 | 
				
			||||||
 | 
					                            // Clear the aggregate buffer
 | 
				
			||||||
 | 
					                            if (aggregateBuffer.length > 0) {
 | 
				
			||||||
 | 
					                                const average = this.getAverage(aggregateBuffer);
 | 
				
			||||||
 | 
					                                this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
 | 
				
			||||||
 | 
					                                aggregateBuffer = [];
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            const gapX = [
 | 
				
			||||||
 | 
					                                lastHeartbeatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
 | 
				
			||||||
 | 
					                                this.$root.unixToDateTime(datapoint.timestamp + 60),
 | 
				
			||||||
 | 
					                            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            for (const x of gapX) {
 | 
				
			||||||
 | 
					                                avgPingData.push({
 | 
				
			||||||
 | 
					                                    x,
 | 
				
			||||||
 | 
					                                    y: null,
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                                minPingData.push({
 | 
				
			||||||
 | 
					                                    x,
 | 
				
			||||||
 | 
					                                    y: null,
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                                maxPingData.push({
 | 
				
			||||||
 | 
					                                    x,
 | 
				
			||||||
 | 
					                                    y: null,
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                                downData.push({
 | 
				
			||||||
 | 
					                                    x,
 | 
				
			||||||
 | 
					                                    y: null,
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                                colorData.push("#000");
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (datapoint.up > 0 && this.chartRawData.length > aggregatePoints * 2) {
 | 
				
			||||||
 | 
					                        // Aggregate Up data using a sliding window
 | 
				
			||||||
 | 
					                        aggregateBuffer.push(datapoint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (aggregateBuffer.length === aggregatePoints) {
 | 
				
			||||||
 | 
					                            const average = this.getAverage(aggregateBuffer);
 | 
				
			||||||
 | 
					                            this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
 | 
				
			||||||
 | 
					                            // Remove the first half of the buffer
 | 
				
			||||||
 | 
					                            aggregateBuffer = aggregateBuffer.slice(Math.floor(aggregatePoints / 2));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        this.heartbeatList = res.data;
 | 
					                        // datapoint is fully down or too few datapoints, no need to aggregate
 | 
				
			||||||
                        this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
 | 
					                        // Clear the aggregate buffer
 | 
				
			||||||
                    }
 | 
					                        if (aggregateBuffer.length > 0) {
 | 
				
			||||||
                    this.loading = false;
 | 
					                            const average = this.getAverage(aggregateBuffer);
 | 
				
			||||||
                });
 | 
					                            this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
 | 
				
			||||||
            }
 | 
					                            aggregateBuffer = [];
 | 
				
			||||||
        }
 | 
					                        }
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    created() {
 | 
					 | 
				
			||||||
        // Setup Watcher on the root heartbeatList,
 | 
					 | 
				
			||||||
        // And mirror latest change to this.heartbeatList
 | 
					 | 
				
			||||||
        this.$watch(() => this.$root.heartbeatList[this.monitorId],
 | 
					 | 
				
			||||||
            (heartbeatList) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
 | 
					                        this.pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                // eslint-disable-next-line eqeqeq
 | 
					 | 
				
			||||||
                if (this.chartPeriodHrs != "0") {
 | 
					 | 
				
			||||||
                    const newBeat = heartbeatList.at(-1);
 | 
					 | 
				
			||||||
                    if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
 | 
					 | 
				
			||||||
                        this.heartbeatList.push(heartbeatList.at(-1));
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    lastHeartbeatTime = beatTime;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					                // Clear the aggregate buffer if there are still datapoints
 | 
				
			||||||
            { deep: true }
 | 
					                if (aggregateBuffer.length > 0) {
 | 
				
			||||||
        );
 | 
					                    const average = this.getAverage(aggregateBuffer);
 | 
				
			||||||
 | 
					                    this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
 | 
				
			||||||
 | 
					                    aggregateBuffer = [];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Load chart period from storage if saved
 | 
					            return {
 | 
				
			||||||
        let period = this.$root.storage()[`chart-period-${this.monitorId}`];
 | 
					                datasets: [
 | 
				
			||||||
        if (period != null) {
 | 
					                    {
 | 
				
			||||||
            this.chartPeriodHrs = Math.min(period, 6);
 | 
					                        // average ping chart
 | 
				
			||||||
        }
 | 
					                        data: avgPingData,
 | 
				
			||||||
 | 
					                        fill: "origin",
 | 
				
			||||||
 | 
					                        tension: 0.2,
 | 
				
			||||||
 | 
					                        borderColor: "#5CDD8B",
 | 
				
			||||||
 | 
					                        backgroundColor: "#5CDD8B06",
 | 
				
			||||||
 | 
					                        yAxisID: "y",
 | 
				
			||||||
 | 
					                        label: "avg-ping",
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        // minimum ping chart
 | 
				
			||||||
 | 
					                        data: minPingData,
 | 
				
			||||||
 | 
					                        fill: "origin",
 | 
				
			||||||
 | 
					                        tension: 0.2,
 | 
				
			||||||
 | 
					                        borderColor: "#3CBD6B38",
 | 
				
			||||||
 | 
					                        backgroundColor: "#5CDD8B06",
 | 
				
			||||||
 | 
					                        yAxisID: "y",
 | 
				
			||||||
 | 
					                        label: "min-ping",
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        // maximum ping chart
 | 
				
			||||||
 | 
					                        data: maxPingData,
 | 
				
			||||||
 | 
					                        fill: "origin",
 | 
				
			||||||
 | 
					                        tension: 0.2,
 | 
				
			||||||
 | 
					                        borderColor: "#7CBD6B38",
 | 
				
			||||||
 | 
					                        backgroundColor: "#5CDD8B06",
 | 
				
			||||||
 | 
					                        yAxisID: "y",
 | 
				
			||||||
 | 
					                        label: "max-ping",
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        // Bar Chart
 | 
				
			||||||
 | 
					                        type: "bar",
 | 
				
			||||||
 | 
					                        data: downData,
 | 
				
			||||||
 | 
					                        borderColor: "#00000000",
 | 
				
			||||||
 | 
					                        backgroundColor: colorData,
 | 
				
			||||||
 | 
					                        yAxisID: "y1",
 | 
				
			||||||
 | 
					                        barThickness: "flex",
 | 
				
			||||||
 | 
					                        barPercentage: 1,
 | 
				
			||||||
 | 
					                        categoryPercentage: 1,
 | 
				
			||||||
 | 
					                        inflateAmount: 0.05,
 | 
				
			||||||
 | 
					                        label: "status",
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -296,6 +567,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            .dark & {
 | 
					            .dark & {
 | 
				
			||||||
                background: $dark-bg;
 | 
					                background: $dark-bg;
 | 
				
			||||||
 | 
					                color: $dark-font-color;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .dark &:hover {
 | 
					            .dark &:hover {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,6 +41,33 @@ export default {
 | 
				
			|||||||
            return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
 | 
					            return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Converts a Unix timestamp to a formatted date and time string.
 | 
				
			||||||
 | 
					         * @param {number} value - The Unix timestamp to convert.
 | 
				
			||||||
 | 
					         * @returns {string} The formatted date and time string.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        unixToDateTime(value) {
 | 
				
			||||||
 | 
					            return dayjs.unix(value).tz(this.timezone).format("YYYY-MM-DD HH:mm:ss");
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Converts a Unix timestamp to a dayjs object.
 | 
				
			||||||
 | 
					         * @param {number} value - The Unix timestamp to convert.
 | 
				
			||||||
 | 
					         * @returns {dayjs.Dayjs} The dayjs object representing the given timestamp.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        unixToDayjs(value) {
 | 
				
			||||||
 | 
					            return dayjs.unix(value).tz(this.timezone);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Converts the given value to a dayjs object.
 | 
				
			||||||
 | 
					         * @param {string} value - the value to be converted
 | 
				
			||||||
 | 
					         * @returns {dayjs.Dayjs} a dayjs object in the timezone of this instance
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        toDayjs(value) {
 | 
				
			||||||
 | 
					            return dayjs.utc(value).tz(this.timezone);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Get time for maintenance
 | 
					         * Get time for maintenance
 | 
				
			||||||
         * @param {string | number | Date | dayjs.Dayjs} value Time to
 | 
					         * @param {string | number | Date | dayjs.Dayjs} value Time to
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -673,6 +673,17 @@ export default {
 | 
				
			|||||||
        getMonitorBeats(monitorID, period, callback) {
 | 
					        getMonitorBeats(monitorID, period, callback) {
 | 
				
			||||||
            socket.emit("getMonitorBeats", monitorID, period, callback);
 | 
					            socket.emit("getMonitorBeats", monitorID, period, callback);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Retrieves monitor chart data.
 | 
				
			||||||
 | 
					         * @param {string} monitorID - The ID of the monitor.
 | 
				
			||||||
 | 
					         * @param {number} period - The time period for the chart data, in hours.
 | 
				
			||||||
 | 
					         * @param {socketCB} callback - The callback function to handle the chart data.
 | 
				
			||||||
 | 
					         * @returns {void}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getMonitorChartData(monitorID, period, callback) {
 | 
				
			||||||
 | 
					            socket.emit("getMonitorChartData", monitorID, period, callback);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    computed: {
 | 
					    computed: {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user