TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { Metadata } from "next";2import Link from "next/link";3import { Badge } from "@/components/ui/badge";4import { formatDate, formatPercent, timeAgo } from "@/lib/format";5import { getStatusSnapshot, type ComponentState, type DayPoint, type Incident } from "@/lib/queries/status";6import { cn } from "@/lib/utils";78export const dynamic = "force-dynamic";910export const metadata: Metadata = {11 title: "Status",12 description: "Live operational status of the Fetcha API, dashboard, proxy networks and authentication, with 24-hour success rate and 30-day history.",13 alternates: { canonical: "/status" },14};1516const STATE_META: Record<ComponentState, { label: string; badge: "success" | "warning" | "danger" | "outline" | "default"; dot: string }> = {17 operational: { label: "Operational", badge: "success", dot: "bg-success" },18 degraded: { label: "Degraded", badge: "warning", dot: "bg-warning" },19 outage: { label: "Major outage", badge: "danger", dot: "bg-danger" },20 "not-launched": { label: "Not launched", badge: "outline", dot: "bg-border-strong" },21 unknown: { label: "Unknown", badge: "default", dot: "bg-fg-subtle" },22};2324const DAY_TONE: Record<DayPoint["state"], string> = {25 operational: "bg-success",26 degraded: "bg-warning",27 outage: "bg-danger",28 "no-data": "bg-bg-muted",29 "not-launched": "bg-bg-muted",30 unknown: "bg-bg-muted",31};3233function dayLabel(p: DayPoint): string {34 const date = new Date(`${p.day}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });35 if (p.state === "no-data" || p.total === 0) return `${date} · No data`;36 return `${date} · ${formatPercent((p.successRate ?? 0) * 100, 2)} success · ${p.total.toLocaleString("en-US")} requests`;37}3839function UptimeBar({ history, name }: { history: DayPoint[]; name: string }) {40 const measured = history.filter((p) => p.state !== "no-data" && p.successRate !== null);41 const avg = measured.length ? measured.reduce((a, p) => a + (p.successRate ?? 0), 0) / measured.length : null;42 return (43 <div>44 <ul className="flex h-7 items-stretch gap-[3px]" aria-label={`${name}: 30-day history`}>45 {history.map((p) => {46 const label = dayLabel(p);47 return (48 <li key={p.day} className={cn("min-w-0 flex-1 rounded-[2px] transition-opacity hover:opacity-80", DAY_TONE[p.state])} title={label}>49 <span className="sr-only">{label}</span>50 </li>51 );52 })}53 </ul>54 <div className="mt-1.5 flex items-center justify-between text-[11.5px] text-fg-subtle">55 <span>30 days ago</span>56 <span className="font-mono tabular">{avg === null ? "No data" : `${formatPercent(avg * 100, 2)} avg. success`}</span>57 <span>Today</span>58 </div>59 </div>60 );61}6263function IncidentItem({ i }: { i: Incident }) {64 const sev = i.severity.toLowerCase();65 return (66 <li className="px-5 py-4">67 <div className="flex flex-wrap items-center gap-2">68 <Badge variant={i.resolvedAt ? "outline" : sev === "major" || sev === "critical" ? "danger" : "warning"} dot>69 {i.resolvedAt ? "Resolved" : sev === "major" || sev === "critical" ? "Major" : "Minor"}70 </Badge>71 <span className="text-[12px] text-fg-subtle">{i.component}</span>72 </div>73 <h3 className="mt-1.5 text-[15px] font-semibold tracking-tight">{i.title}</h3>74 {i.body ? <p className="mt-1 text-[13.5px] leading-relaxed text-fg-muted">{i.body}</p> : null}75 <p className="mt-2 font-mono text-[12px] tabular text-fg-subtle">76 Started {formatDate(i.startedAt)}77 {i.resolvedAt ? ` · Resolved ${formatDate(i.resolvedAt)}` : ` · Ongoing (${timeAgo(i.startedAt)})`}78 </p>79 </li>80 );81}8283export default async function StatusPage() {84 const snap = await getStatusSnapshot();85 const overall = STATE_META[snap.overall];86 const headline =87 snap.overall === "operational" ? "All systems operational" : snap.overall === "degraded" ? "Some systems degraded" : snap.overall === "outage" ? "Major outage in progress" : "Status partially unavailable";8889 return (90 <div className="container-page py-14 sm:py-20">91 <header className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">92 <div>93 <p className="text-[11.5px] font-semibold uppercase tracking-[0.12em] text-accent">Status</p>94 <h1 className="mt-2 flex items-center gap-3 text-[30px] font-semibold leading-tight tracking-tight sm:text-[36px]">95 <span className={cn("size-3 shrink-0 rounded-full", overall.dot, snap.overall === "operational" && "animate-pulse-soft")} aria-hidden />96 {headline}97 </h1>98 <p className="mt-2 text-[13.5px] text-fg-muted">99 Computed live from health checks and request outcomes. Generated <time dateTime={snap.generatedAt.toISOString()}>{formatDate(snap.generatedAt)}</time> UTC.100 </p>101 </div>102 <dl className="grid grid-cols-2 gap-6 sm:text-right">103 <div>104 <dt className="text-[11.5px] uppercase tracking-wide text-fg-subtle">Success rate · 24 h</dt>105 <dd className="mt-0.5 font-mono text-[22px] font-semibold tabular tracking-tight">{snap.successRate24h === null ? "—" : formatPercent(snap.successRate24h * 100, 2)}</dd>106 </div>107 <div>108 <dt className="text-[11.5px] uppercase tracking-wide text-fg-subtle">Requests · 24 h</dt>109 <dd className="mt-0.5 font-mono text-[22px] font-semibold tabular tracking-tight">{snap.requests24h.toLocaleString("en-US")}</dd>110 </div>111 </dl>112 </header>113114 {!snap.dbReachable ? (115 <div role="status" className="mt-8 rounded-lg border border-warning/30 bg-warning-soft px-4 py-3 text-[13.5px] text-fg">116 The status database could not be reached while rendering this page, so some components show as degraded or unknown. This reflects the status page itself, not necessarily the API.117 </div>118 ) : null}119120 {snap.openIncidents.length > 0 ? (121 <section aria-labelledby="open-incidents" className="mt-10">122 <h2 id="open-incidents" className="text-[13px] font-semibold uppercase tracking-wide text-fg-subtle">123 Active incidents124 </h2>125 <ul className="mt-3 divide-y divide-border rounded-lg border border-danger/30 bg-bg-elevated">126 {snap.openIncidents.map((i) => (127 <IncidentItem key={i.id} i={i} />128 ))}129 </ul>130 </section>131 ) : null}132133 <section aria-labelledby="components-title" className="mt-10">134 <h2 id="components-title" className="text-[13px] font-semibold uppercase tracking-wide text-fg-subtle">135 Components136 </h2>137 <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-bg-elevated">138 {snap.components.map((c) => {139 const m = STATE_META[c.state];140 return (141 <li key={c.key} className={cn("px-5 py-5", c.state === "not-launched" && "bg-bg-subtle/50")}>142 <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">143 <div className="min-w-0">144 <h3 className="text-[15.5px] font-semibold tracking-tight">{c.name}</h3>145 <p className="mt-0.5 text-[13px] text-fg-muted">{c.description}</p>146 {c.detail ? <p className="mt-1 text-[12.5px] text-fg-subtle">{c.detail}</p> : null}147 </div>148 <Badge variant={m.badge} dot className="shrink-0 self-start">149 {m.label}150 </Badge>151 </div>152 <div className="mt-4">153 <UptimeBar history={c.history} name={c.name} />154 </div>155 </li>156 );157 })}158 </ul>159 <ul className="mt-3 flex flex-wrap items-center gap-x-5 gap-y-1.5 text-[12px] text-fg-subtle" aria-label="Legend">160 {[161 ["bg-success", "≥ 99% success"],162 ["bg-warning", "95–99% or minor incident"],163 ["bg-danger", "< 95% or major incident"],164 ["bg-bg-muted border border-border", "No data"],165 ].map(([tone, label]) => (166 <li key={label} className="flex items-center gap-1.5">167 <span className={cn("inline-block size-2.5 rounded-[2px]", tone)} aria-hidden />168 {label}169 </li>170 ))}171 </ul>172 </section>173174 <section aria-labelledby="history-title" className="mt-14">175 <h2 id="history-title" className="text-[13px] font-semibold uppercase tracking-wide text-fg-subtle">176 Incident history177 </h2>178 {snap.resolvedIncidents.length === 0 ? (179 <p className="mt-3 rounded-lg border border-dashed border-border px-5 py-8 text-center text-[13.5px] text-fg-muted">No resolved incidents on record.</p>180 ) : (181 <ul className="mt-3 divide-y divide-border rounded-lg border border-border">182 {snap.resolvedIncidents.map((i) => (183 <IncidentItem key={i.id} i={i} />184 ))}185 </ul>186 )}187 </section>188189 <section className="mt-14 grid gap-6 rounded-lg border border-border bg-bg-subtle/60 p-6 text-[13.5px] leading-relaxed text-fg-muted sm:grid-cols-3">190 <div>191 <h2 className="font-semibold text-fg">How this is computed</h2>192 <p className="mt-1">Success rate counts completed requests whose outcome depended on the platform; requests rejected for invalid input, disallowed URLs or exhausted quotas are excluded. Network status comes from continuous upstream health checks.</p>193 </div>194 <div>195 <h2 className="font-semibold text-fg">Machine-readable</h2>196 <p className="mt-1">197 <code className="font-mono text-fg">GET /api/health</code> and <code className="font-mono text-fg">GET /api/ready</code> return JSON you can poll from your own monitoring.198 </p>199 </div>200 <div>201 <h2 className="font-semibold text-fg">Report an issue</h2>202 <p className="mt-1">203 Include the <code className="font-mono text-fg">X-Fetcha-Request-ID</code> from the failing response and email <a className="text-accent underline-offset-4 hover:underline" href="mailto:support@fetcha.co">support@fetcha.co</a>. See the <Link href="/changelog" className="text-accent underline-offset-4 hover:underline">changelog</Link> for releases.204 </p>205 </div>206 </section>207 </div>208 );209}210