From e47a719d79b2522245e339aeebd16fba96316221 Mon Sep 17 00:00:00 2001 From: Shivam Patel Date: Mon, 9 Feb 2026 04:15:34 -0500 Subject: [PATCH] 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. --- app/api/status/route.ts | 144 ++++++++++++++++++------------ components/widgets/UptimeCard.tsx | 16 +++- 2 files changed, 100 insertions(+), 60 deletions(-) diff --git a/app/api/status/route.ts b/app/api/status/route.ts index e4a4fd1..d558bb2 100644 --- a/app/api/status/route.ts +++ b/app/api/status/route.ts @@ -12,73 +12,104 @@ const RANGE_CONFIG: Record= 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) - `); + 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 - FROM uptime_logs - WHERE timestamp > datetime('now', '-24 hours') - GROUP BY service_name - `); + // 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', '-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 = {}; + 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, - sum(case when status = 'up' then 1 else 0 end) as up_count - FROM uptime_logs - GROUP BY service_name - `); + SELECT service_name, + count(*) as total, + sum(case when status = 'up' then 1 else 0 end) as up_count + FROM uptime_logs + 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 - GROUP BY service_name - `); + SELECT service_name, + ROUND(AVG(latency)) as avg_latency + FROM uptime_logs + WHERE timestamp > datetime('now', '-24 hours') + 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, - count(*) as total, - sum(case when status = 'up' then 1 else 0 end) as up_count - FROM uptime_logs - WHERE timestamp > datetime('now', '${config.sqlOffset}') - GROUP BY service_name, bucket - ORDER BY bucket ASC - `); + SELECT service_name, + strftime('${config.strftime}', timestamp) as bucket, + count(*) as total, + sum(case when status = 'up' then 1 else 0 end) as up_count + FROM uptime_logs + WHERE timestamp > datetime('now', '${config.sqlOffset}') + GROUP BY service_name, bucket + ORDER BY bucket ASC + `); - // Group history by service — return up_count and total per bucket const historyMap: Record> = {}; 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] || [], }; diff --git a/components/widgets/UptimeCard.tsx b/components/widgets/UptimeCard.tsx index 2dcd1d6..f52d2d4 100644 --- a/components/widgets/UptimeCard.tsx +++ b/components/widgets/UptimeCard.tsx @@ -59,6 +59,16 @@ const RANGE_CONFIG: Record = { // --- 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'),