Fix 24h bar timezone mismatch and add time-interval uptime calculation
Bars were all grey for 24h view because SQL bucket keys (UTC) never matched frontend keys (local timezone). Added toUTCKey() helper to generate UTC keys for all ranges. Replaced check-counting uptime with time-interval method for 24h/7d: time between consecutive up checks counts as uptime, intervals involving down checks count as downtime, uncovered time is unknown.
This commit is contained in:
@@ -12,41 +12,74 @@ const RANGE_CONFIG: Record<TimeRange, { sqlOffset: string; strftime: string; bar
|
||||
'365d': { sqlOffset: '-365 days', strftime: '%Y-%m', bars: 12 },
|
||||
};
|
||||
|
||||
interface RawLog {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time-interval uptime: time between consecutive "up" checks = uptime.
|
||||
* Intervals involving any "down" check = downtime.
|
||||
* Remaining uncovered time = unknown.
|
||||
* Returns uptime / totalPeriod as a percentage.
|
||||
*/
|
||||
function calculateTimeUptime(
|
||||
logs: RawLog[],
|
||||
periodStartMs: number,
|
||||
periodEndMs: number,
|
||||
): number {
|
||||
const totalMs = periodEndMs - periodStartMs;
|
||||
if (totalMs <= 0 || logs.length < 2) return 0;
|
||||
|
||||
let uptimeMs = 0;
|
||||
|
||||
for (let i = 0; i < logs.length - 1; i++) {
|
||||
const t1 = new Date(logs[i].timestamp).getTime();
|
||||
const t2 = new Date(logs[i + 1].timestamp).getTime();
|
||||
const start = Math.max(t1, periodStartMs);
|
||||
const end = Math.min(t2, periodEndMs);
|
||||
if (start >= end) continue;
|
||||
|
||||
if (logs[i].status === 'up' && logs[i + 1].status === 'up') {
|
||||
uptimeMs += end - start;
|
||||
}
|
||||
}
|
||||
|
||||
return totalMs > 0 ? Math.round((uptimeMs / totalMs) * 100) : 0;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const range = (searchParams.get('range') || '24h') as TimeRange;
|
||||
const config = RANGE_CONFIG[range] || RANGE_CONFIG['24h'];
|
||||
const nowMs = Date.now();
|
||||
const MS_24H = 24 * 3600_000;
|
||||
const MS_7D = 7 * 24 * 3600_000;
|
||||
|
||||
// Get live status (most recent log per service)
|
||||
// Live status (most recent log per service)
|
||||
const live = await db.all(`
|
||||
SELECT service_name as name, url, status, latency
|
||||
FROM uptime_logs
|
||||
WHERE id IN (SELECT MAX(id) FROM uptime_logs GROUP BY service_name)
|
||||
`);
|
||||
|
||||
// Get 24h stats
|
||||
const stats24h = await db.all(`
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
// Raw logs for time-interval uptime (8-day window covers 24h + 7d with buffer)
|
||||
const rawLogs = await db.all(`
|
||||
SELECT service_name, status, timestamp
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
GROUP BY service_name
|
||||
WHERE timestamp > datetime('now', '-8 days')
|
||||
ORDER BY service_name, timestamp ASC
|
||||
`);
|
||||
|
||||
// Get 7d stats
|
||||
const stats7d = await db.all(`
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
sum(case when status = 'up' then 1 else 0 end) as up_count
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-7 days')
|
||||
GROUP BY service_name
|
||||
`);
|
||||
const rawByService: Record<string, RawLog[]> = {};
|
||||
for (const row of rawLogs) {
|
||||
if (!rawByService[row.service_name]) rawByService[row.service_name] = [];
|
||||
rawByService[row.service_name].push({ status: row.status, timestamp: row.timestamp });
|
||||
}
|
||||
|
||||
// Get lifetime stats (all available data)
|
||||
// Lifetime stats (check-counting — efficient for large datasets)
|
||||
const statsLifetime = await db.all(`
|
||||
SELECT service_name,
|
||||
count(*) as total,
|
||||
@@ -55,18 +88,17 @@ export async function GET(request: Request) {
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get average latency over last 24h (only for 'up' checks)
|
||||
// Average latency (24h, up checks only)
|
||||
const avgLatencyRows = await db.all(`
|
||||
SELECT service_name,
|
||||
ROUND(AVG(latency)) as avg_latency
|
||||
FROM uptime_logs
|
||||
WHERE timestamp > datetime('now', '-24 hours')
|
||||
AND status = 'up'
|
||||
AND latency > 0
|
||||
AND status = 'up' AND latency > 0
|
||||
GROUP BY service_name
|
||||
`);
|
||||
|
||||
// Get bucketed history for the requested range
|
||||
// Bucketed history for bar display
|
||||
const history = await db.all(`
|
||||
SELECT service_name,
|
||||
strftime('${config.strftime}', timestamp) as bucket,
|
||||
@@ -78,7 +110,6 @@ export async function GET(request: Request) {
|
||||
ORDER BY bucket ASC
|
||||
`);
|
||||
|
||||
// Group history by service — return up_count and total per bucket
|
||||
const historyMap: Record<string, Array<{ bucket: string; up_count: number; total: number }>> = {};
|
||||
for (const row of history) {
|
||||
if (!historyMap[row.service_name]) historyMap[row.service_name] = [];
|
||||
@@ -89,18 +120,17 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Merge data
|
||||
// Merge results
|
||||
const results = live.map(l => {
|
||||
const s24 = stats24h.find(s => s.service_name === l.name);
|
||||
const s7d = stats7d.find(s => s.service_name === l.name);
|
||||
const sLife = statsLifetime.find(s => s.service_name === l.name);
|
||||
const logs = rawByService[l.name] || [];
|
||||
const avgLat = avgLatencyRows.find(s => s.service_name === l.name);
|
||||
const sLife = statsLifetime.find(s => s.service_name === l.name);
|
||||
|
||||
return {
|
||||
...l,
|
||||
uptime24h: s24 ? Math.round((s24.up_count / s24.total) * 100) : 100,
|
||||
uptime7d: s7d ? Math.round((s7d.up_count / s7d.total) * 100) : 100,
|
||||
uptimeLifetime: sLife ? Math.round((sLife.up_count / sLife.total) * 100) : 100,
|
||||
uptime24h: calculateTimeUptime(logs, nowMs - MS_24H, nowMs),
|
||||
uptime7d: calculateTimeUptime(logs, nowMs - MS_7D, nowMs),
|
||||
uptimeLifetime: sLife ? Math.round((sLife.up_count / sLife.total) * 100) : 0,
|
||||
avgLatency24h: avgLat ? avgLat.avg_latency : 0,
|
||||
history: historyMap[l.name] || [],
|
||||
};
|
||||
|
||||
@@ -59,6 +59,16 @@ const RANGE_CONFIG: Record<TimeRange, { bars: number; bucketMs: number }> = {
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function toUTCKey(d: Date, mode: 'hour' | 'day' | 'month'): string {
|
||||
const Y = d.getUTCFullYear();
|
||||
const M = String(d.getUTCMonth() + 1).padStart(2, '0');
|
||||
if (mode === 'month') return `${Y}-${M}`;
|
||||
const D = String(d.getUTCDate()).padStart(2, '0');
|
||||
if (mode === 'day') return `${Y}-${M}-${D}`;
|
||||
const H = String(d.getUTCHours()).padStart(2, '0');
|
||||
return `${Y}-${M}-${D} ${H}:00`;
|
||||
}
|
||||
|
||||
function bucketStatus(upCount: number, total: number): BarStatus {
|
||||
if (total === 0) return 'unknown';
|
||||
if (upCount === total) return 'up';
|
||||
@@ -81,7 +91,7 @@ function buildBars(range: TimeRange, history: HistoryBucket[] | undefined): BarD
|
||||
if (range === '24h') {
|
||||
for (let i = config.bars - 1; i >= 0; i--) {
|
||||
const d = subHours(now, i);
|
||||
const key = format(d, 'yyyy-MM-dd HH') + ':00';
|
||||
const key = toUTCKey(d, 'hour');
|
||||
const h = bucketMap.get(key);
|
||||
const hourStart = new Date(d);
|
||||
hourStart.setMinutes(0, 0, 0);
|
||||
@@ -95,7 +105,7 @@ function buildBars(range: TimeRange, history: HistoryBucket[] | undefined): BarD
|
||||
} else if (range === '7d' || range === '30d') {
|
||||
for (let i = config.bars - 1; i >= 0; i--) {
|
||||
const d = subDays(now, i);
|
||||
const key = format(d, 'yyyy-MM-dd');
|
||||
const key = toUTCKey(d, 'day');
|
||||
const h = bucketMap.get(key);
|
||||
bars.push({
|
||||
label: format(d, 'MMM d, yyyy'),
|
||||
@@ -108,7 +118,7 @@ function buildBars(range: TimeRange, history: HistoryBucket[] | undefined): BarD
|
||||
// 365d — monthly buckets
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const d = subMonths(now, i);
|
||||
const key = format(d, 'yyyy-MM');
|
||||
const key = toUTCKey(d, 'month');
|
||||
const h = bucketMap.get(key);
|
||||
bars.push({
|
||||
label: format(d, 'MMM yyyy'),
|
||||
|
||||
Reference in New Issue
Block a user