From 76b96804456ee5b18c83bc27d6e2bc0bb4fb930e Mon Sep 17 00:00:00 2001 From: Doruk Date: Sat, 14 Jun 2025 21:58:44 +0200 Subject: [PATCH] bug fix, made beats bars smaller --- server/routers/status-page-router.js | 61 ++++++++++++++-------------- src/components/HeartbeatBar.vue | 55 ++++++++++++++++++++----- 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index c9b60a1b3..6f073e3fd 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -266,40 +266,41 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { const now = dayjs.utc(); const result = []; - // Calculate the actual time range we have - const startTime = now.subtract(days, "day").startOf("minute"); + // Force exact time range: exactly N days ago to exactly now const endTime = now; + const startTime = now.subtract(days, "day"); const totalMinutes = endTime.diff(startTime, "minute"); - // Calculate how many buckets we can actually show (max 100) - const targetBuckets = Math.min(100, totalMinutes); - const bucketSizeMinutes = Math.max(1, Math.floor(totalMinutes / targetBuckets)); + // Always create exactly 100 buckets spanning the full time range + const numBuckets = 100; + const bucketSizeMinutes = totalMinutes / numBuckets; - // Determine data granularity based on days - let dataPoints; - let granularity; + // Get available data from UptimeCalculator for lookup + const availableData = {}; + let rawDataPoints; if (days <= 1) { - // For 1 day or less, use minutely data - granularity = "minute"; - dataPoints = uptimeCalculator.getDataArray(days * 24 * 60, granularity); + const exactMinutes = Math.ceil(days * 24 * 60); + rawDataPoints = uptimeCalculator.getDataArray(exactMinutes, "minute"); } else if (days <= 30) { - // For 2-30 days, use hourly data - granularity = "hour"; - dataPoints = uptimeCalculator.getDataArray(days * 24, granularity); + const exactHours = Math.ceil(days * 24); + rawDataPoints = uptimeCalculator.getDataArray(exactHours, "hour"); } else { - // For 31+ days, use daily data - granularity = "day"; - dataPoints = uptimeCalculator.getDataArray(days, granularity); + rawDataPoints = uptimeCalculator.getDataArray(days, "day"); } - // Create time buckets - const buckets = []; - const actualBuckets = Math.floor(totalMinutes / bucketSizeMinutes); + // Create lookup map for available data + for (const point of rawDataPoints) { + if (point && point.timestamp) { + availableData[point.timestamp] = point; + } + } - for (let i = 0; i < actualBuckets; i++) { + // Create exactly numBuckets buckets spanning the full requested time range + const buckets = []; + for (let i = 0; i < numBuckets; i++) { const bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); - const bucketEnd = bucketStart.add(bucketSizeMinutes, "minute"); + const bucketEnd = startTime.add((i + 1) * bucketSizeMinutes, "minute"); buckets.push({ start: bucketStart.unix(), @@ -312,22 +313,20 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { }); } - // Aggregate data points into buckets - for (const dataPoint of dataPoints) { - if (!dataPoint || !dataPoint.timestamp) { - continue; - } + // Aggregate available data into buckets + for (const [ timestamp, dataPoint ] of Object.entries(availableData)) { + const timestampNum = parseInt(timestamp); // Find the appropriate bucket for this data point const bucket = buckets.find(b => - dataPoint.timestamp >= b.start && dataPoint.timestamp < b.end + timestampNum >= b.start && timestampNum < b.end ); - if (bucket) { + if (bucket && dataPoint) { bucket.up += dataPoint.up || 0; bucket.down += dataPoint.down || 0; - bucket.maintenance += dataPoint.maintenance || 0; - bucket.pending += dataPoint.pending || 0; + bucket.maintenance += 0; // UptimeCalculator treats maintenance as up + bucket.pending += 0; // UptimeCalculator doesn't track pending separately bucket.hasData = true; } } diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index ca98196dc..3a611b63c 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -94,6 +94,12 @@ export default { if (!this.beatList) { return 0; } + + // For configured ranges, no padding needed since we show all beats + if (this.normalizedHeartbeatBarDays > 0) { + return 0; + } + let num = this.beatList.length - this.maxBeat; if (this.move) { @@ -114,17 +120,19 @@ export default { // If heartbeat days is configured (not auto), data should be aggregated from server if (this.normalizedHeartbeatBarDays > 0 && this.beatList.length > 0) { - // Data is already aggregated from server, use it directly - // But still limit to maxBeat for display - if (this.beatList.length > this.maxBeat) { - return this.beatList.slice(this.beatList.length - this.maxBeat); - } + // Data is already aggregated from server covering exact time range requested + // Show all beats to display the full requested time period return this.beatList; } // Original logic for auto mode (heartbeatBarDays = 0) let placeholders = []; + // Handle case where maxBeat is -1 (no limit) + if (this.maxBeat <= 0) { + return this.beatList; + } + let start = this.beatList.length - this.maxBeat; if (this.move) { @@ -144,7 +152,7 @@ export default { wrapStyle() { let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2); - let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2); + let leftRight = (((this.dynamicBeatWidth * this.hoverScale) - this.dynamicBeatWidth) / 2); return { padding: `${topBottom}px ${leftRight}px`, @@ -153,8 +161,8 @@ export default { }, barStyle() { - if (this.move && this.shortBeatList.length > this.maxBeat) { - let width = -(this.beatWidth + this.beatHoverAreaPadding * 2); + if (this.move && this.maxBeat > 0 && this.shortBeatList.length > this.maxBeat) { + let width = -(this.dynamicBeatWidth + this.beatHoverAreaPadding * 2); return { transition: "all ease-in-out 0.25s", @@ -175,9 +183,28 @@ export default { }; }, + /** + * Calculate dynamic beat width for configured ranges + * @returns {number} Beat width in pixels + */ + dynamicBeatWidth() { + // For configured ranges, fit all beats to available width + if (this.normalizedHeartbeatBarDays > 0 && this.shortBeatList.length > 0) { + if (this.$refs.wrap) { + const availableWidth = this.$refs.wrap.clientWidth; + const totalBeats = this.shortBeatList.length; + const totalPadding = totalBeats * (this.beatHoverAreaPadding * 2); + const availableForBeats = availableWidth - totalPadding; + const calculatedWidth = Math.max(1, Math.floor(availableForBeats / totalBeats)); + return Math.min(calculatedWidth, this.beatWidth); // Don't exceed original width + } + } + return this.beatWidth; + }, + beatStyle() { return { - width: this.beatWidth + "px", + width: this.dynamicBeatWidth + "px", height: this.beatHeight + "px", }; }, @@ -188,7 +215,7 @@ export default { */ timeStyle() { return { - "margin-left": this.numPadding * (this.beatWidth + this.beatHoverAreaPadding * 2) + "px", + "margin-left": this.numPadding * (this.dynamicBeatWidth + this.beatHoverAreaPadding * 2) + "px", }; }, @@ -285,7 +312,13 @@ export default { */ resize() { if (this.$refs.wrap) { - this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2)); + // For configured ranges, don't limit maxBeat - show all beats + if (this.normalizedHeartbeatBarDays > 0) { + this.maxBeat = -1; // No limit + } else { + // For auto mode, calculate based on screen width + this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2)); + } } },