diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index a350a1ab8..c9b60a1b3 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -263,50 +263,43 @@ router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, r * @returns {Promise} Array of aggregated heartbeat data */ async function getAggregatedHeartbeats(uptimeCalculator, days) { - const targetBuckets = 100; // Always show ~100 buckets for consistent display const now = dayjs.utc(); const result = []; + // Calculate the actual time range we have + const startTime = now.subtract(days, "day").startOf("minute"); + const endTime = now; + 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)); + // Determine data granularity based on days let dataPoints; let granularity; - let bucketSizeMinutes; if (days <= 1) { // For 1 day or less, use minutely data granularity = "minute"; dataPoints = uptimeCalculator.getDataArray(days * 24 * 60, granularity); - bucketSizeMinutes = Math.max(1, Math.floor((days * 24 * 60) / targetBuckets)); } else if (days <= 30) { // For 2-30 days, use hourly data granularity = "hour"; dataPoints = uptimeCalculator.getDataArray(days * 24, granularity); - bucketSizeMinutes = Math.max(60, Math.floor((days * 24 * 60) / targetBuckets)); } else { // For 31+ days, use daily data granularity = "day"; dataPoints = uptimeCalculator.getDataArray(days, granularity); - bucketSizeMinutes = Math.max(1440, Math.floor((days * 24 * 60) / targetBuckets)); } // Create time buckets - const startTime = now.subtract(days, "day").startOf("minute"); - const endTime = now; const buckets = []; + const actualBuckets = Math.floor(totalMinutes / bucketSizeMinutes); - for (let i = 0; i < targetBuckets; i++) { + for (let i = 0; i < actualBuckets; i++) { const bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); - let bucketEnd = bucketStart.add(bucketSizeMinutes, "minute"); - - // Don't create buckets that start after current time - if (bucketStart.isAfter(endTime)) { - break; - } - - // Ensure bucket doesn't extend beyond current time - if (bucketEnd.isAfter(endTime)) { - bucketEnd = endTime; - } + const bucketEnd = bucketStart.add(bucketSizeMinutes, "minute"); buckets.push({ start: bucketStart.unix(), @@ -374,29 +367,6 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { }); } - // Ensure we always return targetBuckets number of items by padding at the start - while (result.length < targetBuckets) { - const firstStartTime = result.length > 0 ? dayjs(result[0]._startTime || result[0].time) : now.subtract(days, "day"); - const paddedStart = firstStartTime.subtract(bucketSizeMinutes, "minute"); - const paddedEnd = firstStartTime; - - result.unshift({ - status: null, - time: paddedEnd.toISOString(), - msg: "", - ping: null, - _aggregated: true, - _startTime: paddedStart.toISOString(), - _endTime: paddedEnd.toISOString(), - _counts: { - up: 0, - down: 0, - maintenance: 0, - pending: 0 - } - }); - } - return result; } diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index dacf26ac6..fb5834a49 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -155,30 +155,24 @@ export default { // Always do client-side aggregation using fixed time buckets const now = dayjs(); - const buckets = []; - - // Use same logic as server-side: 100 buckets - const targetBuckets = 100; const days = this.normalizedHeartbeatBarDays; - const bucketSizeMinutes = Math.max(1, Math.floor((days * 24 * 60) / targetBuckets)); - // Create time buckets from oldest to newest + // Calculate the actual time range const startTime = now.subtract(days, "day").startOf("minute"); const endTime = now; - - for (let i = 0; i < targetBuckets; i++) { - let bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); - let bucketEnd = bucketStart.add(bucketSizeMinutes, "minute"); + const totalMinutes = endTime.diff(startTime, "minute"); - // Don't create buckets that start after current time - if (bucketStart.isAfter(endTime)) { - break; - } + // Calculate bucket size to fit the display width + const targetBuckets = Math.min(this.maxBeat > 0 ? this.maxBeat : 100, totalMinutes); + const bucketSizeMinutes = Math.max(1, Math.floor(totalMinutes / targetBuckets)); - // Ensure bucket doesn't extend beyond current time - if (bucketEnd.isAfter(endTime)) { - bucketEnd = endTime; - } + // Create time buckets + const buckets = []; + const actualBuckets = Math.floor(totalMinutes / bucketSizeMinutes); + + for (let i = 0; i < actualBuckets; i++) { + const bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); + const bucketEnd = bucketStart.add(bucketSizeMinutes, "minute"); buckets.push({ start: bucketStart, @@ -225,26 +219,6 @@ export default { } }); - // Ensure we always return targetBuckets number of items by padding at the start - while (buckets.length < targetBuckets) { - const firstStart = buckets.length > 0 ? buckets[0].start : now.subtract(days, "day"); - const paddedStart = firstStart.subtract(bucketSizeMinutes, "minute"); - const paddedEnd = firstStart; - - buckets.unshift({ - start: paddedStart, - end: paddedEnd, - beats: [], - status: null, - time: paddedEnd.toISOString() - }); - } - - // Limit to maxBeat for display - if (buckets.length > this.maxBeat) { - return buckets.slice(buckets.length - this.maxBeat); - } - return buckets; }, @@ -429,7 +403,7 @@ export default { // Use start time for display if available const displayTime = beat._startTime ? beat._startTime : beat.time; - + if (total === 0) { return `${this.$root.datetime(displayTime)}: No Data`; }