import { getSupabaseAdmin } from "@/lib/admin"; export interface UptimeCheckResult { websiteId: string; status: "up" | "down" | "warning"; responseTime: number; statusCode: number | null; errorMessage: string | null; sslExpiry: string | null; } /** * Performs a real HTTP check against a URL. * Measures response time, captures status code, and checks SSL certificate expiry. */ async function checkUrl(url: string): Promise> { const start = Date.now(); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); const response = await fetch(url, { method: "HEAD", redirect: "follow", signal: controller.signal, headers: { "User-Agent": "WebsiteMonitor/1.0 (Uptime Check)", }, }); clearTimeout(timeout); const responseTime = Date.now() - start; // Determine status let status: "up" | "down" | "warning" = "up"; if (response.status >= 500) { status = "down"; } else if (response.status >= 400) { status = "warning"; } else if (responseTime > 5000) { status = "warning"; } return { status, responseTime, statusCode: response.status, errorMessage: response.status >= 400 ? `HTTP ${response.status} ${response.statusText}` : null, sslExpiry: null, // SSL expiry checked separately for HTTPS URLs }; } catch (error) { const responseTime = Date.now() - start; const errorMessage = error instanceof Error ? error.message : "Unknown error"; // Differentiate between timeout and connection errors const isTimeout = errorMessage.includes("abort") || errorMessage.includes("timeout"); return { status: "down", responseTime, statusCode: null, errorMessage: isTimeout ? "Request timed out (30s)" : errorMessage, sslExpiry: null, }; } } /** * Fetches all active websites that have monitoring enabled and checks their uptime. */ export async function performUptimeChecks(): Promise<{ checked: number; up: number; down: number; warning: number; errors: string[]; }> { const supabase = getSupabaseAdmin(); const results = { checked: 0, up: 0, down: 0, warning: 0, errors: [] as string[] }; // Get all active websites const { data: websites, error: fetchError } = await supabase .from("websites") .select("id, name, base_url, organization_id") .eq("is_active", true); if (fetchError) { throw new Error(`Failed to fetch websites: ${fetchError.message}`); } if (!websites || websites.length === 0) { return results; } // Check each website in parallel (batched to avoid overwhelming) const batchSize = 10; for (let i = 0; i < websites.length; i += batchSize) { const batch = websites.slice(i, i + batchSize); const checks = await Promise.allSettled( batch.map(async (website) => { try { const result = await checkUrl(String(website.base_url)); // Store result in uptime_checks table const { error: insertError } = await supabase .from("uptime_checks") .insert({ website_id: website.id, status: result.status, response_time: result.responseTime, status_code: result.statusCode, error_message: result.errorMessage, }); if (insertError) { results.errors.push( `DB insert failed for ${website.name}: ${insertError.message}` ); } results.checked++; results[result.status]++; return result; } catch (error) { results.errors.push( `Check failed for ${website.name}: ${error instanceof Error ? error.message : "Unknown"}` ); results.checked++; results.down++; } }) ); } return results; } /** * Evaluates alert rules after uptime checks. * Triggers alerts when thresholds are breached. */ export async function evaluateUptimeAlerts(): Promise { const supabase = getSupabaseAdmin(); let alertsTriggered = 0; // Get alert rules for downtime const { data: alertRules } = await supabase .from("alert_rules") .select("*, websites(name, base_url, organization_id)") .eq("is_active", true); if (!alertRules || alertRules.length === 0) return 0; for (const rule of alertRules) { // Get recent uptime checks for this website const { data: recentChecks } = await supabase .from("uptime_checks") .select("status, response_time, checked_at") .eq("website_id", String(rule.website_id)) .order("checked_at", { ascending: false }) .limit(5); if (!recentChecks || recentChecks.length === 0) continue; let shouldAlert = false; let alertMessage = ""; // Check for consecutive failures const downChecks = recentChecks.filter((c) => c.status === "down"); if (downChecks.length >= 3) { shouldAlert = true; alertMessage = `Website is down — ${downChecks.length} consecutive failures`; } // Check for slow response time const avgResponseTime = recentChecks.reduce((sum, c) => sum + (Number(c.response_time) || 0), 0) / recentChecks.length; if (avgResponseTime > 5000 && !shouldAlert) { shouldAlert = true; alertMessage = `Slow response time — avg ${Math.round(avgResponseTime)}ms over last ${recentChecks.length} checks`; } if (shouldAlert) { // Check if we already sent an alert recently (debounce: 1 hour) const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const { data: recentAlerts } = await supabase .from("alerts") .select("id") .eq("website_id", String(rule.website_id)) .gte("created_at", oneHourAgo) .limit(1); if (recentAlerts && recentAlerts.length > 0) continue; // Already alerted // Create alert record const { error: alertError } = await supabase.from("alerts").insert({ website_id: rule.website_id, alert_rule_id: rule.id, type: "downtime", severity: downChecks.length >= 3 ? "critical" : "warning", message: alertMessage, status: "active", }); if (!alertError) { alertsTriggered++; } } } return alertsTriggered; }