diff --git a/server/model/status_page.js b/server/model/status_page.js index 4cd7474b1..91a894dcb 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -433,7 +433,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 || "auto", + heartbeatBarDays: this.heartbeat_bar_days || 0, }; } diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 074614fa2..5d629949d 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -104,40 +104,23 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques 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); + // For configured day ranges, always use raw heartbeat data for client-side aggregation + // This ensures consistent behavior between edit mode and published mode + const date = new Date(); + date.setDate(date.getDate() - heartbeatBarDays); + const dateFrom = date.toISOString().slice(0, 19).replace("T", " "); - 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 - })); - } + let list = await R.getAll(` + SELECT * FROM heartbeat + WHERE monitor_id = ? AND time >= ? + ORDER BY time DESC + `, [ + monitorID, + dateFrom + ]); + + list = R.convertToBeans("heartbeat", list); + heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); } const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index af43c2f74..a79eb6a35 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -49,8 +49,12 @@ export default { }, /** Heartbeat bar days */ heartbeatBarDays: { - type: Number, + type: [Number, String], default: 0, + validator(value) { + const num = Number(value); + return !isNaN(num) && num >= 0 && num <= 365; + } } }, data() { @@ -65,6 +69,15 @@ export default { }, computed: { + /** + * Normalized heartbeatBarDays as a number + * @returns {number} Number of days for heartbeat bar + */ + normalizedHeartbeatBarDays() { + const num = Number(this.heartbeatBarDays); + return isNaN(num) ? 0 : Math.max(0, Math.min(365, Math.floor(num))); + }, + /** * If heartbeatList is null, get it from $root.heartbeatList * @returns {object} Heartbeat list @@ -103,12 +116,13 @@ export default { return []; } - // If heartbeat days is configured (not auto), aggregate by time periods - if (this.heartbeatBarDays > 0) { + // If heartbeat days is configured (not auto), always use client-side aggregation + // This ensures consistent behavior between edit mode and published mode + if (this.normalizedHeartbeatBarDays > 0) { return this.aggregatedBeatList; } - // Original logic for short time ranges + // Original logic for auto mode (heartbeatBarDays = 0) let placeholders = []; let start = this.beatList.length - this.maxBeat; @@ -138,7 +152,7 @@ export default { const buckets = []; // Calculate total hours from days - const totalHours = this.heartbeatBarDays * 24; + const totalHours = this.normalizedHeartbeatBarDays * 24; // Use dynamic maxBeat calculated from screen size const totalBuckets = this.maxBeat > 0 ? this.maxBeat : 50; @@ -245,7 +259,7 @@ export default { */ timeStyle() { // For aggregated mode, don't use padding-based positioning - if (this.heartbeatBarDays > 0) { + if (this.normalizedHeartbeatBarDays > 0) { return { "margin-left": "0px", }; @@ -263,11 +277,11 @@ export default { */ timeSinceFirstBeat() { // For aggregated beats, calculate from the configured days - if (this.heartbeatBarDays > 0) { - if (this.heartbeatBarDays < 2) { - return (this.heartbeatBarDays * 24) + "h"; + if (this.normalizedHeartbeatBarDays > 0) { + if (this.normalizedHeartbeatBarDays < 2) { + return (this.normalizedHeartbeatBarDays * 24) + "h"; } else { - return this.heartbeatBarDays + "d"; + return this.normalizedHeartbeatBarDays + "d"; } } @@ -371,14 +385,15 @@ export default { return ""; } - // For aggregated beats, show time range and status - if (beat.beats !== undefined && this.heartbeatBarDays > 0) { + // For aggregated beats (client-side aggregation), show time range and status + if (beat.beats !== undefined && this.normalizedHeartbeatBarDays > 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"; return `${start} - ${end}: ${statusText} (${beat.beats.length} checks)`; } + // For published mode with configured days, show simple timestamp return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ""); }, diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue index 7b1169caa..4f3278188 100644 --- a/src/components/PublicGroupList.vue +++ b/src/components/PublicGroupList.vue @@ -117,8 +117,12 @@ export default { }, /** Heartbeat bar days */ heartbeatBarDays: { - type: Number, + type: [Number, String], default: 0, + validator(value) { + const num = Number(value); + return !isNaN(num) && num >= 0 && num <= 365; + } } }, data() { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 0b0dde5be..8d3ab0a48 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -627,6 +627,12 @@ export default { if (res.ok) { this.config = res.config; + if (this.config.heartbeatBarDays === undefined || this.config.heartbeatBarDays === null || this.config.heartbeatBarDays === "") { + this.config.heartbeatBarDays = 0; + } else { + this.config.heartbeatBarDays = parseInt(this.config.heartbeatBarDays, 10) || 0; + } + if (!this.config.customCSS) { this.config.customCSS = "body {\n" + " \n" + @@ -718,8 +724,10 @@ export default { this.config.domainNameList = []; } - if (!this.config.heartbeatBarRange) { - this.config.heartbeatBarRange = "auto"; + if (this.config.heartbeatBarDays === undefined || this.config.heartbeatBarDays === null || this.config.heartbeatBarDays === "") { + this.config.heartbeatBarDays = 0; + } else { + this.config.heartbeatBarDays = parseInt(this.config.heartbeatBarDays, 10) || 0; } if (this.config.icon) {