From 1518fd528b382b57558c460eb7800468140bd37c Mon Sep 17 00:00:00 2001 From: Doruk Date: Sat, 14 Jun 2025 23:09:44 +0200 Subject: [PATCH] address CommanderStorm's feedback + revert statuspage pr --- server/routers/status-page-router.js | 95 ++++++++++++++------------- src/components/HeartbeatBar.vue | 97 ++-------------------------- src/pages/StatusPage.vue | 4 +- 3 files changed, 53 insertions(+), 143 deletions(-) diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index dd003c8db..f45ad3e95 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -89,9 +89,13 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques let statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]); let heartbeatBarDays = statusPage ? (statusPage.heartbeat_bar_days || 0) : 0; - for (let monitorID of monitorIDList) { + // Process all monitors in parallel using Promise.all + const monitorPromises = monitorIDList.map(async (monitorID) => { const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); + let heartbeats; + let uptime; + if (heartbeatBarDays === 0) { // Auto mode - use original LIMIT 100 logic let list = await R.getAll(` @@ -104,13 +108,29 @@ 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()); + heartbeats = list.reverse().map(row => row.toPublicJSON()); + uptime = uptimeCalculator.get24Hour().uptime; } else { // For configured day ranges, use aggregated data from UptimeCalculator - heartbeatList[monitorID] = await getAggregatedHeartbeats(uptimeCalculator, heartbeatBarDays); + heartbeats = await getAggregatedHeartbeats(uptimeCalculator, heartbeatBarDays); + // Calculate uptime for the configured range instead of just 24h + uptime = uptimeCalculator.get24Hour().uptime; // TODO: Calculate range-specific uptime } - uptimeList[`${monitorID}_24`] = uptimeCalculator.get24Hour().uptime; + return { + monitorID, + heartbeats, + uptime + }; + }); + + // Wait for all monitors to be processed + const monitorResults = await Promise.all(monitorPromises); + + // Populate the response objects + for (const result of monitorResults) { + heartbeatList[result.monitorID] = result.heartbeats; + uptimeList[`${result.monitorID}_24`] = result.uptime; } response.json({ @@ -267,18 +287,10 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { const result = []; // 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 optimal bucket count based on available data granularity - // For longer periods with daily data, use fewer buckets to match available data points - let numBuckets = 100; - if (days > 30) { - // For daily data, limit buckets to available data points to prevent sparse data - numBuckets = Math.min(100, Math.max(50, days)); - } - const bucketSizeMinutes = totalMinutes / numBuckets; + const totalMinutes = days * 60 * 24; + const targetBuckets = 100; + const bucketSizeMinutes = totalMinutes / targetBuckets; // Get available data from UptimeCalculator for lookup const availableData = {}; @@ -303,9 +315,9 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { } } - // Create exactly numBuckets buckets spanning the full requested time range + // Create exactly targetBuckets buckets spanning the full requested time range const buckets = []; - for (let i = 0; i < numBuckets; i++) { + for (let i = 0; i < targetBuckets; i++) { const bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); const bucketEnd = startTime.add((i + 1) * bucketSizeMinutes, "minute"); @@ -315,61 +327,48 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) { up: 0, down: 0, maintenance: 0, - pending: 0, - hasData: false + pending: 0 }); } // Aggregate available data into buckets + let bucketIndex = 0; 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 => - timestampNum >= b.start && timestampNum < b.end - ); + // Find the appropriate bucket for this data point (more efficient) + while (bucketIndex < buckets.length - 1 && timestampNum >= buckets[bucketIndex].end) { + bucketIndex++; + } - if (bucket && dataPoint) { + const bucket = buckets[bucketIndex]; + if (bucket && timestampNum >= bucket.start && timestampNum < bucket.end && dataPoint) { bucket.up += dataPoint.up || 0; bucket.down += dataPoint.down || 0; bucket.maintenance += 0; // UptimeCalculator treats maintenance as up bucket.pending += 0; // UptimeCalculator doesn't track pending separately - bucket.hasData = true; } } // Convert buckets to heartbeat format for (const bucket of buckets) { + // Determine status based on priority: DOWN > MAINTENANCE > PENDING > UP let status = null; // No data - - if (bucket.hasData) { - // Determine status based on priority: DOWN > MAINTENANCE > PENDING > UP - if (bucket.down > 0) { - status = DOWN; - } else if (bucket.maintenance > 0) { - status = MAINTENANCE; - } else if (bucket.pending > 0) { - status = PENDING; - } else if (bucket.up > 0) { - status = UP; - } + if (bucket.down > 0) { + status = DOWN; + } else if (bucket.maintenance > 0) { + status = MAINTENANCE; + } else if (bucket.pending > 0) { + status = PENDING; + } else if (bucket.up > 0) { + status = UP; } result.push({ status: status, time: dayjs.unix(bucket.end).toISOString(), msg: "", - ping: null, - // Include aggregation info for client-side display - _aggregated: true, - _startTime: dayjs.unix(bucket.start).toISOString(), - _endTime: dayjs.unix(bucket.end).toISOString(), - _counts: { - up: bucket.up, - down: bucket.down, - maintenance: bucket.maintenance, - pending: bucket.pending - } + ping: null }); } diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index 07bc5d742..7df17a0ba 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -5,7 +5,7 @@ v-for="(beat, index) in shortBeatList" :key="index" class="beat-hover-area" - :class="{ 'empty': (beat === 0 || beat === null || beat.status === null) }" + :class="{ 'empty': (beat === 0) }" :style="beatHoverAreaStyle" :title="getBeatTitle(beat)" > @@ -17,7 +17,7 @@
{{ timeSinceFirstBeat }}
@@ -118,17 +118,9 @@ export default { return []; } - // If heartbeat days is configured (not auto), data should be aggregated from server + // If heartbeat days is configured (not auto), data is already aggregated from server if (this.normalizedHeartbeatBarDays > 0 && this.beatList.length > 0) { - // Calculate how many beats can fit on screen with proper beat size - const maxBeatsOnScreen = this.maxBeat > 0 ? this.maxBeat : 50; // fallback - - // If we have more beats than can fit, aggregate them client-side - if (this.beatList.length > maxBeatsOnScreen) { - return this.aggregateBeats(this.beatList, maxBeatsOnScreen); - } - - // Otherwise show all beats + // Show all beats from server - they are already properly aggregated return this.beatList; } @@ -304,87 +296,6 @@ export default { } }, - /** - * Aggregate beats to fit screen width while maintaining proper beat size - * @param {Array} beats Array of beats to aggregate - * @param {number} targetCount Target number of beats to display - * @returns {Array} Aggregated beats array - */ - aggregateBeats(beats, targetCount) { - if (beats.length <= targetCount) { - return beats; - } - - const aggregated = []; - const beatsPerBucket = beats.length / targetCount; - - for (let i = 0; i < targetCount; i++) { - const startIdx = Math.floor(i * beatsPerBucket); - const endIdx = Math.floor((i + 1) * beatsPerBucket); - const bucketBeats = beats.slice(startIdx, endIdx); - - if (bucketBeats.length === 0) { - continue; - } - - // Aggregate the beats in this bucket - let aggregatedBeat = { - status: null, - time: bucketBeats[bucketBeats.length - 1].time, // Use end time - msg: "", - ping: null, - _aggregated: true, - _startTime: bucketBeats[0]._startTime || bucketBeats[0].time, - _endTime: bucketBeats[bucketBeats.length - 1]._endTime || bucketBeats[bucketBeats.length - 1].time, - _counts: { - up: 0, - down: 0, - maintenance: 0, - pending: 0 - } - }; - - // Sum up counts from all beats in bucket - for (const beat of bucketBeats) { - if (beat && beat._counts) { - aggregatedBeat._counts.up += beat._counts.up || 0; - aggregatedBeat._counts.down += beat._counts.down || 0; - aggregatedBeat._counts.maintenance += beat._counts.maintenance || 0; - aggregatedBeat._counts.pending += beat._counts.pending || 0; - } else if (beat && beat.status !== null) { - // Handle non-aggregated beats - if (beat.status === 1) { - aggregatedBeat._counts.up += 1; - } else if (beat.status === 0) { - aggregatedBeat._counts.down += 1; - } else if (beat.status === 3) { - aggregatedBeat._counts.maintenance += 1; - } else if (beat.status === 2) { - aggregatedBeat._counts.pending += 1; - } - } - } - - // Determine aggregated status (priority: DOWN > MAINTENANCE > PENDING > UP) - const counts = aggregatedBeat._counts; - if (counts.down > 0) { - aggregatedBeat.status = 0; // DOWN - } else if (counts.maintenance > 0) { - aggregatedBeat.status = 3; // MAINTENANCE - } else if (counts.pending > 0) { - aggregatedBeat.status = 2; // PENDING - } else if (counts.up > 0) { - aggregatedBeat.status = 1; // UP - } else { - aggregatedBeat.status = null; // No data - } - - aggregated.push(aggregatedBeat); - } - - return aggregated; - }, - /** * Get the title of the beat. * Used as the hover tooltip on the heartbeat bar. diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 72dfde6b2..8d3ab0a48 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -165,12 +165,12 @@
- - + {{ $t("Go to Dashboard") }}