From ace9ff20b55889f82eb223135203415f482d3861 Mon Sep 17 00:00:00 2001 From: Doruk Date: Sat, 14 Jun 2025 12:49:02 +0200 Subject: [PATCH] simplified to day int instead of custom options accross all components. --- .../2025-06-14-0000-heartbeat-range-config.js | 4 +- server/model/status_page.js | 2 +- server/routers/status-page-router.js | 85 ++++++++------- .../status-page-socket-handler.js | 2 +- server/util/heartbeat-range.js | 101 ------------------ src/components/HeartbeatBar.vue | 45 +++----- src/components/PublicGroupList.vue | 10 +- src/lang/en.json | 13 +-- src/pages/StatusPage.vue | 19 +--- 9 files changed, 78 insertions(+), 203 deletions(-) delete mode 100644 server/util/heartbeat-range.js diff --git a/db/knex_migrations/2025-06-14-0000-heartbeat-range-config.js b/db/knex_migrations/2025-06-14-0000-heartbeat-range-config.js index 6597b8363..5a85a1a39 100644 --- a/db/knex_migrations/2025-06-14-0000-heartbeat-range-config.js +++ b/db/knex_migrations/2025-06-14-0000-heartbeat-range-config.js @@ -1,11 +1,11 @@ exports.up = function (knex) { return knex.schema.alterTable("status_page", function (table) { - table.string("heartbeat_bar_range").defaultTo("auto"); + table.integer("heartbeat_bar_days").notNullable().defaultTo(0); }); }; exports.down = function (knex) { return knex.schema.alterTable("status_page", function (table) { - table.dropColumn("heartbeat_bar_range"); + table.dropColumn("heartbeat_bar_days"); }); }; diff --git a/server/model/status_page.js b/server/model/status_page.js index 6421ef13a..4cd7474b1 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -409,7 +409,7 @@ class StatusPage extends BeanModel { showPoweredBy: !!this.show_powered_by, googleAnalyticsId: this.google_analytics_tag_id, showCertificateExpiry: !!this.show_certificate_expiry, - heartbeatBarRange: this.heartbeat_bar_range, + heartbeatBarDays: this.heartbeat_bar_days, }; } diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 0fe883f08..074614fa2 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -3,7 +3,7 @@ const apicache = require("../modules/apicache"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const StatusPage = require("../model/status_page"); const { allowDevAllOrigin, sendHttpError } = require("../util-server"); -const { getAggregatedHeartbeatData, parseRangeHours } = require("../util/heartbeat-range"); +const dayjs = require("dayjs"); const { R } = require("redbean-node"); const { badgeConstants } = require("../../src/util"); const { makeBadge } = require("badge-maker"); @@ -87,48 +87,57 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques // Get the status page to determine the heartbeat range let statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]); - let heartbeatRange = statusPage ? statusPage.heartbeat_bar_range : "auto"; + let heartbeatBarDays = statusPage ? (statusPage.heartbeat_bar_days || 0) : 0; for (let monitorID of monitorIDList) { - let list; - - // Try to use aggregated data from stat tables for better performance - const aggregatedData = await getAggregatedHeartbeatData(monitorID, heartbeatRange); - - if (aggregatedData) { - // Use pre-aggregated stat data - heartbeatList[monitorID] = aggregatedData; - } else { - // Fall back to raw heartbeat data (auto mode or no stat data) - if (heartbeatRange === "auto") { - // Auto mode - use original LIMIT 100 logic - list = await R.getAll(` - SELECT * FROM heartbeat - WHERE monitor_id = ? - ORDER BY time DESC - LIMIT 100 - `, [ - monitorID, - ]); - } else { - // Non-auto range but no stat data - filter raw heartbeat data by time - const hours = parseRangeHours(heartbeatRange); - const date = new Date(); - date.setHours(date.getHours() - hours); - const dateFrom = date.toISOString().slice(0, 19).replace("T", " "); - - list = await R.getAll(` - SELECT * FROM heartbeat - WHERE monitor_id = ? AND time >= ? - ORDER BY time DESC - `, [ - monitorID, - dateFrom - ]); - } + if (heartbeatBarDays === 0) { + // Auto mode - use original LIMIT 100 logic + let list = await R.getAll(` + SELECT * FROM heartbeat + WHERE monitor_id = ? + ORDER BY time DESC + LIMIT 100 + `, [ + monitorID, + ]); list = R.convertToBeans("heartbeat", list); heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); + } else { + // Use UptimeCalculator for configured day ranges + const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); + + if (heartbeatBarDays <= 1) { + // Use 24-hour data + const data = uptimeCalculator.get24Hour(); + heartbeatList[monitorID] = Object.entries(data.minutelyUptimeDataList || {}).map(([ timestamp, uptimeData ]) => ({ + time: dayjs(parseInt(timestamp)).format("YYYY-MM-DD HH:mm:ss"), + status: uptimeData.up > 0 ? 1 : (uptimeData.down > 0 ? 0 : 1), + up: uptimeData.up, + down: uptimeData.down, + ping: uptimeData.avgPing + })); + } else if (heartbeatBarDays <= 30) { + // Use 30-day hourly data + const data = uptimeCalculator.get30Day(); + heartbeatList[monitorID] = Object.entries(data.hourlyUptimeDataList || {}).map(([ timestamp, uptimeData ]) => ({ + time: dayjs(parseInt(timestamp)).format("YYYY-MM-DD HH:mm:ss"), + status: uptimeData.up > 0 ? 1 : (uptimeData.down > 0 ? 0 : 1), + up: uptimeData.up, + down: uptimeData.down, + ping: uptimeData.avgPing + })); + } else { + // Use daily data for longer ranges + const data = uptimeCalculator.getData(); + heartbeatList[monitorID] = Object.entries(data.dailyUptimeDataList || {}).map(([ timestamp, uptimeData ]) => ({ + time: dayjs(parseInt(timestamp)).format("YYYY-MM-DD HH:mm:ss"), + status: uptimeData.up > 0 ? 1 : (uptimeData.down > 0 ? 0 : 1), + up: uptimeData.up, + down: uptimeData.down, + ping: uptimeData.avgPing + })); + } } const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 15f59ee69..adf57a4fd 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -165,7 +165,7 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.custom_css = config.customCSS; statusPage.show_powered_by = config.showPoweredBy; statusPage.show_certificate_expiry = config.showCertificateExpiry; - statusPage.heartbeat_bar_range = config.heartbeatBarRange; + statusPage.heartbeat_bar_days = config.heartbeatBarDays; statusPage.modified_date = R.isoDateTime(); statusPage.google_analytics_tag_id = config.googleAnalyticsId; diff --git a/server/util/heartbeat-range.js b/server/util/heartbeat-range.js deleted file mode 100644 index be3c32de4..000000000 --- a/server/util/heartbeat-range.js +++ /dev/null @@ -1,101 +0,0 @@ -const { R } = require("redbean-node"); -const dayjs = require("dayjs"); - -/** - * Utility functions for heartbeat range handling - */ - -/** - * Parse heartbeat range string and return hours - * @param {string} range - Range string like "6h", "7d", "auto" - * @returns {number|null} Hours or null for auto - */ -function parseRangeHours(range) { - if (!range || range === "auto") { - return null; - } - - if (range.endsWith("h")) { - return parseInt(range); - } else if (range.endsWith("d")) { - return parseInt(range) * 24; - } - - // Fallback - return 90 * 24; -} - -/** - * Get aggregated heartbeat data using stat tables for better performance - * @param {number} monitorId - Monitor ID - * @param {string} range - Range string like "6h", "7d", "auto" - * @returns {Promise} Aggregated heartbeat data - */ -async function getAggregatedHeartbeatData(monitorId, range) { - if (!range || range === "auto") { - return null; - } - - const now = dayjs(); - const hours = parseRangeHours(range); - - if (hours <= 24) { - // Use hourly stats for ranges up to 24 hours - const startTime = now.subtract(hours, "hours"); - const timestampKey = Math.floor(startTime.valueOf() / (60 * 60 * 1000)); // Convert to seconds - - const stats = await R.getAll(` - SELECT * FROM stat_hourly - WHERE monitor_id = ? AND timestamp >= ? - ORDER BY timestamp ASC - `, [ monitorId, timestampKey ]); - - // If no stat data, fall back to raw heartbeat data - if (stats.length === 0) { - return null; // This will trigger fallback in router - } - - // Convert stat data to simplified format for client-side aggregation - const result = stats.map(stat => ({ - time: dayjs(stat.timestamp * 1000).format("YYYY-MM-DD HH:mm:ss"), - status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), - up: stat.up, - down: stat.down, - ping: stat.ping - })); - - return result; - } else { - // Use daily stats for ranges over 24 hours - const days = Math.ceil(hours / 24); - const startTime = now.subtract(days, "days"); - const timestampKey = Math.floor(startTime.valueOf() / (24 * 60 * 60 * 1000)); // Convert to seconds - - const stats = await R.getAll(` - SELECT * FROM stat_daily - WHERE monitor_id = ? AND timestamp >= ? - ORDER BY timestamp ASC - `, [ monitorId, timestampKey ]); - - // If no stat data, fall back to raw heartbeat data - if (stats.length === 0) { - return null; // This will trigger fallback in router - } - - // Convert stat data to simplified format for client-side aggregation - const result = stats.map(stat => ({ - time: dayjs(stat.timestamp * 1000).format("YYYY-MM-DD HH:mm:ss"), - status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), - up: stat.up, - down: stat.down, - ping: stat.ping - })); - - return result; - } -} - -module.exports = { - parseRangeHours, - getAggregatedHeartbeatData -}; diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index 92c6c01f3..af43c2f74 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -47,10 +47,10 @@ export default { type: Array, default: null, }, - /** Heartbeat bar range */ - heartbeatBarRange: { - type: String, - default: "auto", + /** Heartbeat bar days */ + heartbeatBarDays: { + type: Number, + default: 0, } }, data() { @@ -103,8 +103,8 @@ export default { return []; } - // If heartbeat range is configured (not auto), aggregate by time periods - if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { + // If heartbeat days is configured (not auto), aggregate by time periods + if (this.heartbeatBarDays > 0) { return this.aggregatedBeatList; } @@ -137,15 +137,8 @@ export default { const now = dayjs(); const buckets = []; - // Parse range to get total hours - let totalHours; - if (this.heartbeatBarRange.endsWith("h")) { - totalHours = parseInt(this.heartbeatBarRange); - } else if (this.heartbeatBarRange.endsWith("d")) { - totalHours = parseInt(this.heartbeatBarRange) * 24; - } else { - totalHours = 90 * 24; // Fallback - } + // Calculate total hours from days + const totalHours = this.heartbeatBarDays * 24; // Use dynamic maxBeat calculated from screen size const totalBuckets = this.maxBeat > 0 ? this.maxBeat : 50; @@ -252,7 +245,7 @@ export default { */ timeStyle() { // For aggregated mode, don't use padding-based positioning - if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { + if (this.heartbeatBarDays > 0) { return { "margin-left": "0px", }; @@ -269,18 +262,12 @@ export default { * @returns {string} The time elapsed in minutes or hours. */ timeSinceFirstBeat() { - // For aggregated beats, calculate from the configured range - if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { - if (this.heartbeatBarRange.endsWith("h")) { - const hours = parseInt(this.heartbeatBarRange); - return hours + "h"; - } else if (this.heartbeatBarRange.endsWith("d")) { - const days = parseInt(this.heartbeatBarRange); - if (days < 2) { - return (days * 24) + "h"; - } else { - return days + "d"; - } + // For aggregated beats, calculate from the configured days + if (this.heartbeatBarDays > 0) { + if (this.heartbeatBarDays < 2) { + return (this.heartbeatBarDays * 24) + "h"; + } else { + return this.heartbeatBarDays + "d"; } } @@ -385,7 +372,7 @@ export default { } // For aggregated beats, show time range and status - if (beat.beats !== undefined && this.heartbeatBarRange && this.heartbeatBarRange !== "auto") { + if (beat.beats !== undefined && this.heartbeatBarDays > 0) { const start = this.$root.datetime(beat.start); const end = this.$root.datetime(beat.end); const statusText = beat.status === 1 ? "Up" : beat.status === 0 ? "Down" : beat.status === 3 ? "Maintenance" : "No Data"; diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue index 608209389..7b1169caa 100644 --- a/src/components/PublicGroupList.vue +++ b/src/components/PublicGroupList.vue @@ -73,7 +73,7 @@
- +
@@ -115,10 +115,10 @@ export default { showCertificateExpiry: { type: Boolean, }, - /** Heartbeat bar range */ - heartbeatBarRange: { - type: String, - default: "auto", + /** Heartbeat bar days */ + heartbeatBarDays: { + type: Number, + default: 0, } }, data() { diff --git a/src/lang/en.json b/src/lang/en.json index 91067505d..552f454f0 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -375,17 +375,8 @@ "Footer Text": "Footer Text", "Refresh Interval": "Refresh Interval", "Refresh Interval Description": "The status page will do a full site refresh every {0} seconds", - "Heartbeat Bar Range": "Heartbeat Bar Range", - "6 hours": "6 hours", - "12 hours": "12 hours", - "24 hours": "24 hours", - "7 days": "7 days", - "30 days": "30 days", - "60 days": "60 days", - "90 days": "90 days", - "180 days": "180 days", - "365 days": "365 days", - "How much heartbeat history to show in the status page": "How much heartbeat history to show in the status page", + "Heartbeat Bar Days": "Heartbeat Bar Days", + "Number of days of heartbeat history to show (0 = auto)": "Number of days of heartbeat history to show (0 = auto)", "Show Powered By": "Show Powered By", "Domain Names": "Domain Names", "signedInDisp": "Signed in as {0}", diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index b31356e48..0b0dde5be 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -43,21 +43,10 @@
- - + +
- {{ $t("How much heartbeat history to show in the status page") }} + {{ $t("Number of days of heartbeat history to show (0 = auto)") }}
@@ -347,7 +336,7 @@ 👀 {{ $t("statusPageNothing") }} - +