From adc362a2a8f31a586951d5f8e3d55b8c85c633fb Mon Sep 17 00:00:00 2001 From: Doruk Date: Sat, 14 Jun 2025 23:42:40 +0200 Subject: [PATCH] rethought beat aggregation system fully server-side --- server/routers/status-page-router.js | 9 ++++++--- src/components/HeartbeatBar.vue | 19 ++++++++++++++++++- src/pages/StatusPage.vue | 27 +++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index f45ad3e95..d8dd96904 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -89,6 +89,9 @@ 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; + // Get max beats parameter from query string (for client-side screen width constraints) + const maxBeats = parseInt(request.query.maxBeats) || 100; + // Process all monitors in parallel using Promise.all const monitorPromises = monitorIDList.map(async (monitorID) => { const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID); @@ -112,7 +115,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques uptime = uptimeCalculator.get24Hour().uptime; } else { // For configured day ranges, use aggregated data from UptimeCalculator - heartbeats = await getAggregatedHeartbeats(uptimeCalculator, heartbeatBarDays); + heartbeats = await getAggregatedHeartbeats(uptimeCalculator, heartbeatBarDays, maxBeats); // Calculate uptime for the configured range instead of just 24h uptime = uptimeCalculator.get24Hour().uptime; // TODO: Calculate range-specific uptime } @@ -280,16 +283,16 @@ router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, r * Get aggregated heartbeats for status page display * @param {UptimeCalculator} uptimeCalculator The uptime calculator instance * @param {number} days Number of days to show + * @param {number} targetBuckets Number of buckets to aggregate into (default 100) * @returns {Promise} Array of aggregated heartbeat data */ -async function getAggregatedHeartbeats(uptimeCalculator, days) { +async function getAggregatedHeartbeats(uptimeCalculator, days, targetBuckets = 100) { const now = dayjs.utc(); const result = []; // Force exact time range: exactly N days ago to exactly now const startTime = now.subtract(days, "day"); const totalMinutes = days * 60 * 24; - const targetBuckets = 100; const bucketSizeMinutes = totalMinutes / targetBuckets; // Get available data from UptimeCalculator for lookup diff --git a/src/components/HeartbeatBar.vue b/src/components/HeartbeatBar.vue index 7df17a0ba..059c9fedc 100644 --- a/src/components/HeartbeatBar.vue +++ b/src/components/HeartbeatBar.vue @@ -292,7 +292,24 @@ export default { */ resize() { if (this.$refs.wrap) { - this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2)); + const newMaxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2)); + + // If maxBeat changed and we're in configured days mode, notify parent to reload data + if (newMaxBeat !== this.maxBeat && this.normalizedHeartbeatBarDays > 0) { + this.maxBeat = newMaxBeat; + console.log(`HeartBeat Debug: Container width changed, maxBeat=${newMaxBeat}, notifying parent`); + + // Find the closest parent with reloadHeartbeatData method (StatusPage) + let parent = this.$parent; + while (parent && !parent.reloadHeartbeatData) { + parent = parent.$parent; + } + if (parent && parent.reloadHeartbeatData) { + parent.reloadHeartbeatData(newMaxBeat); + } + } else { + this.maxBeat = newMaxBeat; + } } }, diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 8d3ab0a48..d399dcb3d 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -787,10 +787,19 @@ export default { /** * Load heartbeat data from API + * @param {number|null} maxBeats Maximum number of beats to request from server * @returns {Promise} Promise that resolves when data is loaded */ - loadHeartbeatData() { - return axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => { + loadHeartbeatData(maxBeats = null) { + // If maxBeats is provided (from HeartbeatBar resize), use it + // Otherwise, use a default that will be updated when components mount + const targetMaxBeats = maxBeats || 50; // Default, will be updated by actual container measurement + + console.log(`HeartBeat Debug: Using maxBeats=${targetMaxBeats}, provided=${maxBeats !== null}`); + + return axios.get("/api/status-page/heartbeat/" + this.slug, { + params: { maxBeats: targetMaxBeats } + }).then((res) => { const { heartbeatList, uptimeList } = res.data; this.$root.heartbeatList = heartbeatList; @@ -844,6 +853,20 @@ export default { }, 1000); }, + /** + * Reload heartbeat data with specific maxBeats count + * Called by child components when they determine optimal beat count + * @param {number} maxBeats Maximum number of beats that fit in container + * @returns {void} + */ + reloadHeartbeatData(maxBeats) { + // Only reload if we have configured days (not auto mode) + if (this.config && this.config.heartbeatBarDays > 0) { + console.log(`HeartBeat Debug: Reloading with maxBeats=${maxBeats} for ${this.config.heartbeatBarDays} days`); + this.loadHeartbeatData(maxBeats); + } + }, + /** * Enable editing mode * @returns {void}