diff --git a/data/status-history.json b/data/status-history.json new file mode 100644 index 00000000..740f0fee --- /dev/null +++ b/data/status-history.json @@ -0,0 +1,39 @@ +{ + "history": [ + { + "date": "2026-02-03", + "uptime": 99.98, + "incidents": 0 + }, + { + "date": "2026-02-04", + "uptime": 99.92, + "incidents": 1 + }, + { + "date": "2026-02-05", + "uptime": 99.87, + "incidents": 1 + }, + { + "date": "2026-02-06", + "uptime": 100, + "incidents": 0 + }, + { + "date": "2026-02-07", + "uptime": 99.99, + "incidents": 0 + }, + { + "date": "2026-02-08", + "uptime": 99.95, + "incidents": 1 + }, + { + "date": "2026-02-09", + "uptime": 100, + "incidents": 0 + } + ] +} diff --git a/src/app/api/status/history/route.ts b/src/app/api/status/history/route.ts new file mode 100644 index 00000000..029df8ba --- /dev/null +++ b/src/app/api/status/history/route.ts @@ -0,0 +1,40 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { NextResponse } from "next/server"; + +interface StatusHistoryEntry { + date: string; + uptime: number; + incidents: number; +} + +interface StatusHistoryData { + history: StatusHistoryEntry[]; +} + +const STATUS_HISTORY_PATH = path.join(process.cwd(), "data", "status-history.json"); + +export async function GET() { + try { + const file = await readFile(STATUS_HISTORY_PATH, "utf8"); + const parsed = JSON.parse(file) as StatusHistoryData; + + return NextResponse.json(parsed, { + headers: { + "Cache-Control": "no-store", + }, + }); + } catch { + return NextResponse.json( + { + history: [], + }, + { + status: 500, + headers: { + "Cache-Control": "no-store", + }, + } + ); + } +} diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts new file mode 100644 index 00000000..1e004993 --- /dev/null +++ b/src/app/api/status/route.ts @@ -0,0 +1,154 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { NextRequest, NextResponse } from "next/server"; + +type StatusLevel = "operational" | "degraded" | "down"; + +interface ServiceHealth { + name: string; + status: StatusLevel; + latencyMs: number; + lastCheck: string; +} + +const execFileAsync = promisify(execFile); + +function createOriginFromRequest(request: NextRequest): string { + const protocol = request.headers.get("x-forwarded-proto") ?? "https"; + const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host"); + + if (!host) return "http://localhost:3000"; + + return `${protocol}://${host}`; +} + +function resolveOverallStatus(services: ServiceHealth[]): StatusLevel { + if (services.some((service) => service.status === "down")) return "down"; + if (services.some((service) => service.status === "degraded")) return "degraded"; + return "operational"; +} + +async function runSelfCheck(origin: string): Promise { + const startedAt = Date.now(); + let status: StatusLevel = "operational"; + + try { + const response = await fetch(`${origin}/api/health`, { + method: "GET", + cache: "no-store", + signal: AbortSignal.timeout(5000), + }); + + if (!response.ok) { + status = response.status >= 500 ? "down" : "degraded"; + } + } catch { + status = "down"; + } + + return { + name: "API", + status, + latencyMs: Date.now() - startedAt, + lastCheck: new Date().toISOString(), + }; +} + +async function runSupabaseCheck(): Promise { + const startedAt = Date.now(); + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; + + let status: StatusLevel = "operational"; + + if (!supabaseUrl || !supabaseAnonKey) { + status = "degraded"; + } else { + try { + const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/rest/v1/`, { + method: "GET", + headers: { + apikey: supabaseAnonKey, + Authorization: `Bearer ${supabaseAnonKey}`, + }, + cache: "no-store", + signal: AbortSignal.timeout(5000), + }); + + if (response.status >= 500) { + status = "down"; + } else if (response.status >= 400) { + status = "degraded"; + } + } catch { + status = "down"; + } + } + + return { + name: "Supabase", + status, + latencyMs: Date.now() - startedAt, + lastCheck: new Date().toISOString(), + }; +} + +async function runGitHubCheck(): Promise { + const startedAt = Date.now(); + let status: StatusLevel = "operational"; + + try { + const { stdout } = await execFileAsync("gh", ["api", "rate_limit"], { + timeout: 5000, + maxBuffer: 1024 * 1024, + }); + + const parsed = JSON.parse(stdout) as { + resources?: { + core?: { + remaining?: number; + }; + }; + }; + + const remaining = parsed.resources?.core?.remaining; + if (typeof remaining !== "number") { + status = "degraded"; + } else if (remaining <= 0) { + status = "down"; + } else if (remaining < 100) { + status = "degraded"; + } + } catch { + status = "down"; + } + + return { + name: "GitHub API", + status, + latencyMs: Date.now() - startedAt, + lastCheck: new Date().toISOString(), + }; +} + +export async function GET(request: NextRequest) { + const origin = createOriginFromRequest(request); + + const services = await Promise.all([ + runSelfCheck(origin), + runSupabaseCheck(), + runGitHubCheck(), + ]); + + return NextResponse.json( + { + services, + overall: resolveOverallStatus(services), + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); +} diff --git a/src/app/status/page.tsx b/src/app/status/page.tsx index 4c1a4f8b..dfef272e 100644 --- a/src/app/status/page.tsx +++ b/src/app/status/page.tsx @@ -1,26 +1,31 @@ "use client"; -import { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useEffect, useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; +import { Card, CardContent } from "@/components/ui/card"; -type ServiceStatus = "operational" | "degraded" | "outage"; +type ServiceStatus = "operational" | "degraded" | "down"; interface Service { name: string; status: ServiceStatus; - uptime: number; - responseTime: number; + latencyMs: number; + lastCheck: string; +} + +interface StatusResponse { + services: Service[]; + overall: ServiceStatus; } -interface Incident { - id: string; +interface HistoryEntry { date: string; - title: string; - description: string; - status: "resolved" | "investigating" | "monitoring"; - affectedServices: string[]; + uptime: number; + incidents: number; +} + +interface HistoryResponse { + history: HistoryEntry[]; } const statusConfig = { @@ -34,130 +39,77 @@ const statusConfig = { text: "Degraded", badgeVariant: "secondary" as const, }, - outage: { + down: { color: "bg-red-500", - text: "Outage", + text: "Down", badgeVariant: "destructive" as const, }, }; -// Mock data - in production this would come from an API -const initialServices: Service[] = [ - { - name: "API", - status: "operational", - uptime: 99.98, - responseTime: 145, - }, - { - name: "Website", - status: "operational", - uptime: 99.99, - responseTime: 87, - }, - { - name: "MCP Registry", - status: "operational", - uptime: 99.95, - responseTime: 213, - }, - { - name: "Supabase", - status: "operational", - uptime: 99.97, - responseTime: 92, - }, - { - name: "CDN", - status: "operational", - uptime: 100.0, - responseTime: 34, - }, -]; - -const incidents: Incident[] = [ - { - id: "inc-005", - date: "2026-02-05", - title: "Brief API Latency Spike", - description: - "Users experienced elevated API response times for approximately 8 minutes. Root cause identified as database connection pool exhaustion. Connection limits have been increased.", - status: "resolved", - affectedServices: ["API"], - }, - { - id: "inc-004", - date: "2026-01-28", - title: "CDN Cache Invalidation Issue", - description: - "CDN cache invalidation failed for approximately 15 minutes, causing some users to see stale content. Issue was resolved by manually purging cache and redeploying edge configuration.", - status: "resolved", - affectedServices: ["CDN", "Website"], - }, - { - id: "inc-003", - date: "2026-01-22", - title: "MCP Registry Search Degradation", - description: - "Search functionality in the MCP Registry experienced degraded performance due to an index optimization job running during peak hours. Job has been rescheduled to off-peak times.", - status: "resolved", - affectedServices: ["MCP Registry"], - }, - { - id: "inc-002", - date: "2026-01-15", - title: "Supabase Connection Timeout", - description: - "Database connection timeouts affected user authentication for approximately 12 minutes. Supabase team resolved the issue on their end. Implementing additional retry logic.", - status: "resolved", - affectedServices: ["Supabase", "API"], - }, - { - id: "inc-001", - date: "2026-01-09", - title: "Scheduled Maintenance", - description: - "Planned maintenance window for infrastructure upgrades. All services were briefly unavailable for approximately 30 minutes. Upgrades included security patches and performance improvements.", - status: "resolved", - affectedServices: ["API", "Website", "MCP Registry", "Supabase", "CDN"], - }, -]; - export default function StatusPage() { - const [services] = useState(initialServices); - const [lastUpdated, setLastUpdated] = useState(new Date()); + const [services, setServices] = useState([]); + const [overallStatus, setOverallStatus] = useState("operational"); + const [history, setHistory] = useState([]); + const [lastUpdated, setLastUpdated] = useState(null); + const [error, setError] = useState(null); const webPageJsonLd = { "@context": "https://schema.org", "@type": "WebPage", name: "Status — forAgents.dev", - description: - "Real-time status and uptime information for forAgents.dev services including API, Website, MCP Registry, Supabase, and CDN.", + description: "Real-time status and uptime information for forAgents.dev services.", url: "https://foragents.dev/status", }; - // Simulate periodic updates (in production, this would poll an API) useEffect(() => { - const interval = setInterval(() => { - setLastUpdated(new Date()); - }, 60000); // Update every minute - - return () => clearInterval(interval); + let mounted = true; + + const fetchStatus = async () => { + try { + const [statusRes, historyRes] = await Promise.all([ + fetch("/api/status", { cache: "no-store" }), + fetch("/api/status/history", { cache: "no-store" }), + ]); + + if (!statusRes.ok || !historyRes.ok) { + throw new Error("Failed to load status"); + } + + const statusData = (await statusRes.json()) as StatusResponse; + const historyData = (await historyRes.json()) as HistoryResponse; + + if (!mounted) return; + + setServices(statusData.services); + setOverallStatus(statusData.overall); + setHistory(historyData.history); + setLastUpdated(new Date()); + setError(null); + } catch { + if (!mounted) return; + setError("Unable to load live status checks right now."); + } + }; + + fetchStatus(); + const interval = setInterval(fetchStatus, 30_000); + + return () => { + mounted = false; + clearInterval(interval); + }; }, []); - const overallStatus: ServiceStatus = - services.every((s) => s.status === "operational") - ? "operational" - : services.some((s) => s.status === "outage") - ? "outage" - : "degraded"; + const overallHeading = useMemo(() => { + if (overallStatus === "operational") return "All Systems Operational"; + if (overallStatus === "degraded") return "Some Systems Degraded"; + return "Service Outage"; + }, [overallStatus]); const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { + return new Date(dateString).toLocaleDateString("en-US", { month: "short", day: "numeric", - year: "numeric", }); }; @@ -168,53 +120,35 @@ export default function StatusPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(webPageJsonLd) }} /> - {/* Hero Section with Overall Status */} -
-
-
-
- +

System Status

-

- Real-time status and uptime information -

+

Live health checks, refreshed every 30 seconds

- {/* Overall Status Banner */} -
-
-
-
-

- {overallStatus === "operational" - ? "All Systems Operational" - : overallStatus === "degraded" - ? "Some Systems Degraded" - : "Service Outage"} -

-

- Last updated: {lastUpdated.toLocaleTimeString()} -

-
+
+
+
+

{overallHeading}

+

+ {lastUpdated ? `Last updated: ${lastUpdated.toLocaleTimeString()}` : "Loading..."} +

+ + {error ?

{error}

: null}
- {/* Services Status */} -
-

Services

- +
+

Services

{services.map((service) => (
-
+

{service.name}

- + {statusConfig[service.status].text}
-

Uptime (30d)

-

- {service.uptime.toFixed(2)}% -

+

Latency

+

{service.latencyMs}ms

-

Response Time

+

Last check

- {service.responseTime}ms + {new Date(service.lastCheck).toLocaleTimeString()}

@@ -257,80 +184,30 @@ export default function StatusPage() {
- {/* Incident History */} -
-

Incident History

- -
- {incidents.map((incident) => ( - - -
-
-
- - {incident.title} - - - {incident.status} - +
+

Uptime (Last 7 Days)

+ + + +
+ {history.map((entry) => { + const heightPct = Math.max(8, Math.round(entry.uptime)); + + return ( +
+
+
-

- {formatDate(incident.date)} -

+

{formatDate(entry.date)}

+

{entry.uptime.toFixed(2)}%

+

Incidents: {entry.incidents}

-
- - -

- {incident.description} -

- -
-

- Affected Services: -

-
- {incident.affectedServices.map((service) => ( - - {service} - - ))} -
-
-
- - ))} -
-
- - {/* Subscribe to Updates */} -
- - -

- Stay Updated -

-

- Subscribe to status updates and be notified of incidents as they happen. -

-

- Coming soon: Email and webhook notifications -

+ ); + })} +