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 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;
|
||||||
|
|
||||||
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);
|
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||||
|
|
||||||
|
let heartbeats;
|
||||||
|
let uptime;
|
||||||
|
|
||||||
if (heartbeatBarDays === 0) {
|
if (heartbeatBarDays === 0) {
|
||||||
// Auto mode - use original LIMIT 100 logic
|
// Auto mode - use original LIMIT 100 logic
|
||||||
let list = await R.getAll(`
|
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);
|
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 {
|
} else {
|
||||||
// For configured day ranges, use aggregated data from UptimeCalculator
|
// 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({
|
response.json({
|
||||||
|
@ -267,18 +287,10 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) {
|
||||||
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 endTime = now;
|
|
||||||
const startTime = now.subtract(days, "day");
|
const startTime = now.subtract(days, "day");
|
||||||
const totalMinutes = endTime.diff(startTime, "minute");
|
const totalMinutes = days * 60 * 24;
|
||||||
|
const targetBuckets = 100;
|
||||||
// Calculate optimal bucket count based on available data granularity
|
const bucketSizeMinutes = totalMinutes / targetBuckets;
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Get available data from UptimeCalculator for lookup
|
// Get available data from UptimeCalculator for lookup
|
||||||
const availableData = {};
|
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 = [];
|
const buckets = [];
|
||||||
for (let i = 0; i < numBuckets; i++) {
|
for (let i = 0; i < targetBuckets; i++) {
|
||||||
const bucketStart = startTime.add(i * bucketSizeMinutes, "minute");
|
const bucketStart = startTime.add(i * bucketSizeMinutes, "minute");
|
||||||
const bucketEnd = startTime.add((i + 1) * bucketSizeMinutes, "minute");
|
const bucketEnd = startTime.add((i + 1) * bucketSizeMinutes, "minute");
|
||||||
|
|
||||||
|
@ -315,61 +327,48 @@ async function getAggregatedHeartbeats(uptimeCalculator, days) {
|
||||||
up: 0,
|
up: 0,
|
||||||
down: 0,
|
down: 0,
|
||||||
maintenance: 0,
|
maintenance: 0,
|
||||||
pending: 0,
|
pending: 0
|
||||||
hasData: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate available data into buckets
|
// Aggregate available data into buckets
|
||||||
|
let bucketIndex = 0;
|
||||||
for (const [ timestamp, dataPoint ] of Object.entries(availableData)) {
|
for (const [ timestamp, dataPoint ] of Object.entries(availableData)) {
|
||||||
const timestampNum = parseInt(timestamp);
|
const timestampNum = parseInt(timestamp);
|
||||||
|
|
||||||
// Find the appropriate bucket for this data point
|
// Find the appropriate bucket for this data point (more efficient)
|
||||||
const bucket = buckets.find(b =>
|
while (bucketIndex < buckets.length - 1 && timestampNum >= buckets[bucketIndex].end) {
|
||||||
timestampNum >= b.start && timestampNum < b.end
|
bucketIndex++;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (bucket && dataPoint) {
|
const bucket = buckets[bucketIndex];
|
||||||
|
if (bucket && timestampNum >= bucket.start && timestampNum < bucket.end && dataPoint) {
|
||||||
bucket.up += dataPoint.up || 0;
|
bucket.up += dataPoint.up || 0;
|
||||||
bucket.down += dataPoint.down || 0;
|
bucket.down += dataPoint.down || 0;
|
||||||
bucket.maintenance += 0; // UptimeCalculator treats maintenance as up
|
bucket.maintenance += 0; // UptimeCalculator treats maintenance as up
|
||||||
bucket.pending += 0; // UptimeCalculator doesn't track pending separately
|
bucket.pending += 0; // UptimeCalculator doesn't track pending separately
|
||||||
bucket.hasData = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert buckets to heartbeat format
|
// Convert buckets to heartbeat format
|
||||||
for (const bucket of buckets) {
|
for (const bucket of buckets) {
|
||||||
|
// Determine status based on priority: DOWN > MAINTENANCE > PENDING > UP
|
||||||
let status = null; // No data
|
let status = null; // No data
|
||||||
|
if (bucket.down > 0) {
|
||||||
if (bucket.hasData) {
|
status = DOWN;
|
||||||
// Determine status based on priority: DOWN > MAINTENANCE > PENDING > UP
|
} else if (bucket.maintenance > 0) {
|
||||||
if (bucket.down > 0) {
|
status = MAINTENANCE;
|
||||||
status = DOWN;
|
} else if (bucket.pending > 0) {
|
||||||
} else if (bucket.maintenance > 0) {
|
status = PENDING;
|
||||||
status = MAINTENANCE;
|
} else if (bucket.up > 0) {
|
||||||
} else if (bucket.pending > 0) {
|
status = UP;
|
||||||
status = PENDING;
|
|
||||||
} else if (bucket.up > 0) {
|
|
||||||
status = UP;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
status: status,
|
status: status,
|
||||||
time: dayjs.unix(bucket.end).toISOString(),
|
time: dayjs.unix(bucket.end).toISOString(),
|
||||||
msg: "",
|
msg: "",
|
||||||
ping: null,
|
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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
v-for="(beat, index) in shortBeatList"
|
v-for="(beat, index) in shortBeatList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="beat-hover-area"
|
class="beat-hover-area"
|
||||||
:class="{ 'empty': (beat === 0 || beat === null || beat.status === null) }"
|
:class="{ 'empty': (beat === 0) }"
|
||||||
:style="beatHoverAreaStyle"
|
:style="beatHoverAreaStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:title="getBeatTitle(beat)"
|
||||||
>
|
>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
|
||||||
>
|
>
|
||||||
<div>{{ timeSinceFirstBeat }}</div>
|
<div>{{ timeSinceFirstBeat }}</div>
|
||||||
|
@ -118,17 +118,9 @@ export default {
|
||||||
return [];
|
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) {
|
if (this.normalizedHeartbeatBarDays > 0 && this.beatList.length > 0) {
|
||||||
// Calculate how many beats can fit on screen with proper beat size
|
// Show all beats from server - they are already properly aggregated
|
||||||
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
|
|
||||||
return this.beatList;
|
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.
|
* Get the title of the beat.
|
||||||
* Used as the hover tooltip on the heartbeat bar.
|
* Used as the hover tooltip on the heartbeat bar.
|
||||||
|
|
|
@ -165,12 +165,12 @@
|
||||||
<!-- Admin functions -->
|
<!-- Admin functions -->
|
||||||
<div v-if="hasToken" class="mb-4">
|
<div v-if="hasToken" class="mb-4">
|
||||||
<div v-if="!enableEditMode">
|
<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" />
|
<font-awesome-icon icon="edit" />
|
||||||
{{ $t("Edit Status Page") }}
|
{{ $t("Edit Status Page") }}
|
||||||
</button>
|
</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" />
|
<font-awesome-icon icon="tachometer-alt" />
|
||||||
{{ $t("Go to Dashboard") }}
|
{{ $t("Go to Dashboard") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
Loading…
Add table
Reference in a new issue