SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
10.4 KB · 210 lines tsx
Raw Blame History
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