rethought beat aggregation system fully server-side

This commit is contained in:
Doruk 2025-06-14 23:42:40 +02:00
parent 1518fd528b
commit adc362a2a8
3 changed files with 49 additions and 6 deletions

View file

@ -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 statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]);
let heartbeatBarDays = statusPage ? (statusPage.heartbeat_bar_days || 0) : 0; 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 // Process all monitors in parallel using Promise.all
const monitorPromises = monitorIDList.map(async (monitorID) => { const monitorPromises = monitorIDList.map(async (monitorID) => {
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(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; uptime = uptimeCalculator.get24Hour().uptime;
} else { } else {
// For configured day ranges, use aggregated data from UptimeCalculator // 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 // Calculate uptime for the configured range instead of just 24h
uptime = uptimeCalculator.get24Hour().uptime; // TODO: Calculate range-specific uptime 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 * Get aggregated heartbeats for status page display
* @param {UptimeCalculator} uptimeCalculator The uptime calculator instance * @param {UptimeCalculator} uptimeCalculator The uptime calculator instance
* @param {number} days Number of days to show * @param {number} days Number of days to show
* @param {number} targetBuckets Number of buckets to aggregate into (default 100)
* @returns {Promise<Array>} Array of aggregated heartbeat data * @returns {Promise<Array>} Array of aggregated heartbeat data
*/ */
async function getAggregatedHeartbeats(uptimeCalculator, days) { async function getAggregatedHeartbeats(uptimeCalculator, days, targetBuckets = 100) {
const now = dayjs.utc(); const now = dayjs.utc();
const result = []; const result = [];
// Force exact time range: exactly N days ago to exactly now // Force exact time range: exactly N days ago to exactly now
const startTime = now.subtract(days, "day"); const startTime = now.subtract(days, "day");
const totalMinutes = days * 60 * 24; const totalMinutes = days * 60 * 24;
const targetBuckets = 100;
const bucketSizeMinutes = totalMinutes / targetBuckets; const bucketSizeMinutes = totalMinutes / targetBuckets;
// Get available data from UptimeCalculator for lookup // Get available data from UptimeCalculator for lookup

View file

@ -292,7 +292,24 @@ export default {
*/ */
resize() { resize() {
if (this.$refs.wrap) { 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;
}
} }
}, },

View file

@ -787,10 +787,19 @@ export default {
/** /**
* Load heartbeat data from API * 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 * @returns {Promise} Promise that resolves when data is loaded
*/ */
loadHeartbeatData() { loadHeartbeatData(maxBeats = null) {
return axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => { // 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; const { heartbeatList, uptimeList } = res.data;
this.$root.heartbeatList = heartbeatList; this.$root.heartbeatList = heartbeatList;
@ -844,6 +853,20 @@ export default {
}, 1000); }, 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 * Enable editing mode
* @returns {void} * @returns {void}