mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-19 18:56:48 +02:00
address CommanderStorm's feedback + revert statuspage pr
This commit is contained in:
parent
1f4c4a0cb1
commit
1518fd528b
3 changed files with 53 additions and 143 deletions
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue