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'),