address CommanderStorm's feedback + revert statuspage pr

This commit is contained in:
Doruk 2025-06-14 23:09:44 +02:00
parent 1f4c4a0cb1
commit 1518fd528b
3 changed files with 53 additions and 143 deletions

View file

@ -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
});
}

View file

@ -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 @@
</div>
</div>
<div
v-if="!$root.isMobile && size !== 'small' && shortBeatList.length > 4 && $root.styleElapsedTime !== 'none'"
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }}</div>
@ -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.

View file

@ -165,12 +165,12 @@
<!-- Admin functions -->
<div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode">
<button class="btn btn-primary me-2" data-testid="edit-button" @click="edit">
<button class="btn btn-info me-2" data-testid="edit-button" @click="edit">
<font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }}
</button>
<a href="/manage-status-page" class="btn btn-primary">
<a href="/manage-status-page" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>