import "server-only"; import { getDb, providerHealth, fetchRequests, statusIncidents, sql, gte, and, ne, or, isNull, isNotNull, desc } from "@fetcha/db"; import { internalApi } from "@/lib/api"; export type ComponentState = "operational" | "degraded" | "outage" | "not-launched" | "unknown"; export type DayPoint = { day: string; state: ComponentState | "no-data"; successRate: number | null; total: number }; export type StatusComponent = { key: string; name: string; description: string; state: ComponentState; detail?: string; history: DayPoint[]; // 30 entries, oldest first }; export type Incident = { id: string; component: string; title: string; body: string | null; severity: string; startedAt: Date; resolvedAt: Date | null; }; export type StatusSnapshot = { generatedAt: Date; overall: ComponentState; components: StatusComponent[]; successRate24h: number | null; requests24h: number; openIncidents: Incident[]; resolvedIncidents: Incident[]; dbReachable: boolean; }; const EXCLUDED_ERROR_CODES = ["INVALID_REQUEST", "URL_NOT_ALLOWED", "USAGE_LIMIT_REACHED"] as const; const HISTORY_DAYS = 30; const COMPONENT_DEFS: Array<{ key: string; name: string; description: string }> = [ { key: "api", name: "API", description: "POST /v1/fetch, sessions, usage and authentication endpoints." }, { key: "dashboard", name: "Dashboard", description: "Web console, playground and request logs." }, { key: "proxy-network", name: "Proxy Network", description: "Aggregate upstream capacity across all network classes." }, { key: "residential-network", name: "Residential Network", description: "Residential exit nodes with geo targeting." }, { key: "browser-network", name: "Browser Network", description: "Managed headless browser execution." }, { key: "authentication", name: "Authentication", description: "Sign-in, sign-up, email verification and API key validation." }, { key: "billing", name: "Billing", description: "Checkout, invoices and self-serve plan changes." }, ]; function utcDayKey(d: Date): string { return d.toISOString().slice(0, 10); } function healthToState(rows: Array<{ status: string }>): ComponentState { if (rows.some((r) => r.status === "healthy")) return "operational"; if (rows.some((r) => r.status === "degraded")) return "degraded"; return "outage"; } function rateToState(rate: number | null, total: number): DayPoint["state"] { if (rate === null || total === 0) return "no-data"; if (rate >= 0.99) return "operational"; if (rate >= 0.95) return "degraded"; return "outage"; } function worst(states: ComponentState[]): ComponentState { const rank: Record = { outage: 4, degraded: 3, unknown: 2, operational: 1, "not-launched": 0 }; return states.reduce((acc, s) => (rank[s] > rank[acc] ? s : acc), "operational"); } function emptyHistory(state: DayPoint["state"]): DayPoint[] { const out: DayPoint[] = []; const today = new Date(); for (let i = HISTORY_DAYS - 1; i >= 0; i--) { const d = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - i)); out.push({ day: utcDayKey(d), state, successRate: null, total: 0 }); } return out; } /** Marks days overlapped by incidents for a given component as degraded/outage; everything else stays as provided. */ function overlayIncidents(history: DayPoint[], incidents: Incident[], componentKey: string, componentName: string): DayPoint[] { const mine = incidents.filter((i) => { const c = i.component.toLowerCase(); return c === componentKey || c === componentName.toLowerCase() || c === componentKey.replace(/-/g, "_"); }); if (mine.length === 0) return history; return history.map((p) => { const dayStart = new Date(`${p.day}T00:00:00.000Z`).getTime(); const dayEnd = dayStart + 86_400_000; const hit = mine.filter((i) => i.startedAt.getTime() < dayEnd && (i.resolvedAt ? i.resolvedAt.getTime() : Date.now()) >= dayStart); if (hit.length === 0) return p; const major = hit.some((i) => ["major", "critical"].includes(i.severity.toLowerCase())); return { ...p, state: major ? "outage" : "degraded" }; }); } export async function getStatusSnapshot(): Promise { const now = new Date(); const since24h = new Date(now.getTime() - 24 * 3600 * 1000); const since30d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - (HISTORY_DAYS - 1))); let dbReachable = true; let healthRows: Array<{ network: string; status: string }> = []; let successRate24h: number | null = null; let requests24h = 0; let daily: Map = new Map(); let openIncidents: Incident[] = []; let resolvedIncidents: Incident[] = []; // --- Provider health (never expose provider names; we only read network + status). try { const db = getDb(); healthRows = await db .select({ network: providerHealth.network, status: providerHealth.status }) .from(providerHealth) .where(gte(providerHealth.checkedAt, since24h)) .orderBy(desc(providerHealth.checkedAt)) .limit(500); } catch { dbReachable = false; } const excludeErrors = or(isNull(fetchRequests.errorCode), sql`${fetchRequests.errorCode} not in (${sql.join(EXCLUDED_ERROR_CODES.map((c) => sql`${c}`), sql`, `)})`); // --- 24 h success rate. if (dbReachable) { try { const db = getDb(); const [row] = await db .select({ total: sql`count(*)::int`, ok: sql`count(*) filter (where ${fetchRequests.status} = 'success')::int`, }) .from(fetchRequests) .where(and(gte(fetchRequests.createdAt, since24h), ne(fetchRequests.status, "pending"), excludeErrors)); const total = Number(row?.total ?? 0); const ok = Number(row?.ok ?? 0); requests24h = total; successRate24h = total > 0 ? ok / total : null; } catch { dbReachable = false; } } // --- 30 day daily buckets. if (dbReachable) { try { const db = getDb(); const rows = await db .select({ day: sql`date_trunc('day', ${fetchRequests.createdAt} at time zone 'UTC')`.as("day"), total: sql`count(*)::int`, ok: sql`count(*) filter (where ${fetchRequests.status} = 'success')::int`, }) .from(fetchRequests) .where(and(gte(fetchRequests.createdAt, since30d), ne(fetchRequests.status, "pending"), excludeErrors)) .groupBy(sql`1`) .orderBy(sql`1`); daily = new Map( rows.map((r) => { const d = r.day instanceof Date ? r.day : new Date(r.day); return [utcDayKey(d), { total: Number(r.total), ok: Number(r.ok) }]; }), ); } catch { dbReachable = false; } } // --- Incidents. if (dbReachable) { try { const db = getDb(); const toIncident = (r: typeof statusIncidents.$inferSelect): Incident => ({ id: r.id, component: r.component, title: r.title, body: r.body, severity: r.severity, startedAt: new Date(r.startedAt), resolvedAt: r.resolvedAt ? new Date(r.resolvedAt) : null, }); const open = await db.select().from(statusIncidents).where(isNull(statusIncidents.resolvedAt)).orderBy(desc(statusIncidents.startedAt)).limit(20); const resolved = await db.select().from(statusIncidents).where(isNotNull(statusIncidents.resolvedAt)).orderBy(desc(statusIncidents.startedAt)).limit(10); openIncidents = open.map(toIncident); resolvedIncidents = resolved.map(toIncident); } catch { // incidents are optional; keep whatever we have } } // --- API readiness via the internal API. let apiState: ComponentState = "operational"; let apiDetail: string | undefined; try { const ready = await internalApi.ready(); const s = String(ready.status ?? "").toLowerCase(); apiState = s === "ok" || s === "ready" || s === "healthy" ? "operational" : "degraded"; if (apiState === "degraded") apiDetail = "Readiness check reports a degraded dependency."; } catch { apiState = "degraded"; apiDetail = "Readiness check unreachable from the status page."; } // --- Build the 30-day history from daily fetch success rates. const trafficHistory: DayPoint[] = emptyHistory("no-data").map((p) => { const b = daily.get(p.day); if (!b || b.total === 0) return p; const rate = b.ok / b.total; return { day: p.day, total: b.total, successRate: rate, state: rateToState(rate, b.total) }; }); const allIncidents = [...openIncidents, ...resolvedIncidents]; const residentialRows = healthRows.filter((r) => r.network === "residential"); const residentialState: ComponentState = dbReachable ? healthToState(residentialRows) : "unknown"; const proxyState: ComponentState = dbReachable ? healthToState(healthRows) : "unknown"; const dashboardState: ComponentState = dbReachable ? "operational" : "degraded"; const authState: ComponentState = dbReachable ? "operational" : "degraded"; const rate24Detail = successRate24h === null ? "No requests in the last 24 hours." : `${(successRate24h * 100).toFixed(2)}% success over ${requests24h.toLocaleString("en-US")} requests (24 h).`; const components: StatusComponent[] = COMPONENT_DEFS.map((def) => { switch (def.key) { case "api": return { ...def, state: apiState, detail: apiDetail ?? rate24Detail, history: overlayIncidents(trafficHistory, allIncidents, def.key, def.name) }; case "dashboard": return { ...def, state: dashboardState, detail: dbReachable ? undefined : "Database unreachable while rendering this page.", history: overlayIncidents(emptyHistory("operational"), allIncidents, def.key, def.name) }; case "proxy-network": return { ...def, state: proxyState, detail: !dbReachable ? "Health data unavailable." : healthRows.length === 0 ? "No health checks recorded in the last 24 hours." : undefined, history: overlayIncidents(trafficHistory, allIncidents, def.key, def.name), }; case "residential-network": return { ...def, state: residentialState, detail: !dbReachable ? "Health data unavailable." : residentialRows.length === 0 ? "No residential health checks recorded in the last 24 hours." : rate24Detail, history: overlayIncidents(trafficHistory, allIncidents, def.key, def.name), }; case "browser-network": return { ...def, state: "not-launched", detail: "Browser execution is not yet available (BROWSER_UNAVAILABLE).", history: emptyHistory("no-data") }; case "authentication": return { ...def, state: authState, detail: dbReachable ? undefined : "Database unreachable while rendering this page.", history: overlayIncidents(emptyHistory("operational"), allIncidents, def.key, def.name) }; case "billing": return { ...def, state: "not-launched", detail: "Checkout is not live. Upgrades by email to sales@fetcha.co.", history: emptyHistory("no-data") }; default: return { ...def, state: "unknown", history: emptyHistory("no-data") }; } }); const overall = worst(components.map((c) => c.state).filter((s) => s !== "not-launched")); return { generatedAt: now, overall, components, successRate24h, requests24h, openIncidents, resolvedIncidents, dbReachable }; }