160 lines6,536 bytesCodeBlame
1import { useEffect } from "react";
2import { Link, useRevalidator } from "react-router";
3
4import type { Route } from "./+types/status";
5import { TrustPage } from "../components/trust-page";
6import { StatusDot } from "../components/footer";
7import { TimeAgo } from "../components/ui";
8import { CONTACT } from "../lib/legal";
9import { page } from "../lib/meta";
10import { type ComponentStatus, INCIDENT_DAYS, type Incident, STATE_LABEL, dotClass } from "../lib/status";
11import { currentStatus } from "../lib/status.server";
12
13export function meta({ loaderData, ...args }: Route.MetaArgs) {
14 return page(args, {
15 title: "Status · g1t",
16 description: loaderData
17 ? `${loaderData.overall.line}. Live status of g1t: the website, API, git, MCP, docs, deployments, agents and billing.`
18 : "Live status of g1t: the website, API, git, MCP, docs, deployments, agents and billing.",
19 });
20}
21
22export async function loader() {
23 return currentStatus();
24}
25
26/** How often an open page checks again: as often as the checks run. */
27const REFRESH_MS = 60_000;
28
29const BANNER: Record<string, string> = {
30 up: "border-accent/30 bg-accent/5",
31 degraded: "border-warn/40 bg-warn/5",
32 down: "border-danger/40 bg-danger/5",
33 unknown: "border-line bg-surface",
34};
35
36const STATE_TEXT: Record<ComponentStatus["state"], string> = {
37 up: "text-accent",
38 degraded: "text-warn",
39 down: "text-danger",
40 unmonitored: "text-faint",
41};
42
43function ComponentRow({ component }: { component: ComponentStatus }) {
44 return (
45 <li className="flex flex-col gap-2 px-4 py-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
46 <div className="min-w-0">
47 <p className="flex flex-wrap items-center gap-x-2 font-medium">
48 <span aria-hidden className={`size-2 shrink-0 rounded-full ${dotClass(component.state)}`} />
49 {component.name}
50 {/* Beside the name when there's room, under it on a phone. */}
51 <span className="w-full truncate pl-4 font-mono text-xs font-normal text-faint sm:w-auto sm:pl-0">
52 {component.address}
53 </span>
54 </p>
55 <p className="mt-1 pl-4 text-sm text-muted">{component.checks}</p>
56 </div>
57 <div className="shrink-0 pl-4 sm:pl-0 sm:text-right">
58 <p className={`text-sm font-medium ${STATE_TEXT[component.state]}`}>{STATE_LABEL[component.state]}</p>
59 {component.state !== "unmonitored" && <p className="text-xs text-faint">{component.detail}</p>}
60 </div>
61 </li>
62 );
63}
64
65function IncidentItem({ incident }: { incident: Incident }) {
66 const open = incident.resolved_at == null;
67 return (
68 <li className="px-4 py-4">
69 <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
70 <p className="font-medium">{incident.title}</p>
71 <p className={`text-xs font-medium ${open ? (incident.impact === "down" ? "text-danger" : "text-warn") : "text-faint"}`}>
72 {open ? "Ongoing" : "Resolved"}
73 </p>
74 </div>
75 <p className="mt-0.5 text-xs text-faint">
76 Started <TimeAgo at={incident.started_at} />
77 {incident.resolved_at && (
78 <>
79 {" "}· resolved <TimeAgo at={incident.resolved_at} />
80 </>
81 )}
82 </p>
83 {incident.updates.length > 0 && (
84 <ol className="mt-3 space-y-2 border-l border-line pl-4 text-sm">
85 {incident.updates.map((update) => (
86 <li key={update.at}>
87 <span className="text-faint">
88 <TimeAgo at={update.at} />:
89 </span>{" "}
90 <span className="text-fg/90">{update.text}</span>
91 </li>
92 ))}
93 </ol>
94 )}
95 </li>
96 );
97}
98
99export default function Status({ loaderData }: Route.ComponentProps) {
100 const { overall, components, incidents, checked_at } = loaderData;
101 const revalidator = useRevalidator();
102 // Keep an open page current, without the visitor reloading it.
103 useEffect(() => {
104 const timer = setInterval(() => {
105 if (document.visibilityState === "visible" && revalidator.state === "idle") revalidator.revalidate();
106 }, REFRESH_MS);
107 return () => clearInterval(timer);
108 }, [revalidator]);
109 const monitored = components.filter((c) => c.state !== "unmonitored").length;
110 return (
111 <TrustPage eyebrow="Status" title="g1t status">
112 <div className="max-w-3xl">
113 <div className={`flex items-start gap-3 rounded-xl border px-5 py-4 ${BANNER[overall.state]}`}>
114 <StatusDot state={overall.state} className="mt-2" />
115 <div className="min-w-0">
116 <p className="text-lg font-semibold tracking-tight">{overall.line}</p>
117 <p className="mt-0.5 text-sm text-muted">
118 {monitored} of {components.length} parts checked, last <TimeAgo at={checked_at} />. Checked again every minute.
119 </p>
120 </div>
121 </div>
122
123 <ul className="mt-8 divide-y divide-line rounded-xl border border-line">
124 {components.map((component) => (
125 <ComponentRow key={component.key} component={component} />
126 ))}
127 </ul>
128 <p className="mt-3 text-xs text-faint">
129 Each check is one quick request, the same a visitor's would make, from g1t's own servers. A part that
130 answers in over 1.5 seconds shows as degraded; one that fails or takes over 3 seconds, as down. Parts we
131 can't check yet say so, rather than showing green. The same data is at{" "}
132 <a href="/status.json" className="font-mono hover:text-fg">
133 /status.json
134 </a>
135 .
136 </p>
137
138 <section className="mt-14">
139 <h2 className="text-xl font-semibold tracking-tight">Incidents</h2>
140 {incidents.length === 0 ? (
141 <p className="mt-3 text-sm text-muted">No incidents reported in the last {INCIDENT_DAYS} days.</p>
142 ) : (
143 <ul className="mt-4 divide-y divide-line rounded-xl border border-line">
144 {incidents.map((incident) => (
145 <IncidentItem key={incident.id} incident={incident} />
146 ))}
147 </ul>
148 )}
149 <p className="mt-4 text-sm text-muted">
150 Something broken that this page doesn't show? Tell us at{" "}
151 <a href={`mailto:${CONTACT.support}`} className="text-accent hover:underline">
152 {CONTACT.support}
153 </a>
154 , or see <Link to="/support" className="text-accent hover:underline">support</Link>.
155 </p>
156 </section>
157 </div>
158 </TrustPage>
159 );
160}