g1t/apps/sudo/app/routes/maintenance-new.tsx

112 lines5,523 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1import { redirect } from "react-router";
2
3import type { Route } from "./+types/maintenance-new";
4import { BackLink } from "~/components/incidents";
5import { Button, Field, Input, Notice, PageHeader, Section, Textarea } from "~/components/ui";
6import { text } from "~/lib/forms";
7import { localValue, utc } from "~/lib/incidents";
8import { statusAdmin } from "~/lib/services.server";
9import { settle } from "~/lib/settle";
Fast pages, required checks on the branch, self-hosted runners, honest incidents10import { requireStaff, zoneContext } from "~/lib/staff";
11import { zoneAbbr } from "~/lib/time";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12
13export const meta: Route.MetaFunction = () => [{ title: "Schedule maintenance · sudo" }, { name: "robots", content: "noindex, nofollow" }];
14
15export async function loader({ context }: Route.LoaderArgs) {
16 requireStaff(context);
Fast pages, required checks on the branch, self-hosted runners, honest incidents17 const { zone } = context.get(zoneContext);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas18 const components = await settle(statusAdmin.components());
Fast pages, required checks on the branch, self-hosted runners, honest incidents19 // A sensible default: tomorrow, 02:00 to 03:00 UTC, shown in the staff member's zone.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas20 const now = new Date();
21 const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 2));
22 return {
23 components: components.ok ? components.value : [],
24 error: components.ok ? null : components.error,
Fast pages, required checks on the branch, self-hosted runners, honest incidents25 start: localValue(start, zone),
26 end: localValue(new Date(start.getTime() + 3600_000), zone),
27 zone: zoneAbbr(start, zone),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas28 };
29}
30
31type ActionData = { error: string; values: Record<string, string>; chosen: string[] };
32
33export async function action({ request, context }: Route.ActionArgs) {
34 const staff = requireStaff(context);
Fast pages, required checks on the branch, self-hosted runners, honest incidents35 const { zone } = context.get(zoneContext);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas36 const form = await request.formData();
37 const values = Object.fromEntries(["title", "message", "starts_at", "ends_at"].map((k) => [k, text(form, k)]));
38 const chosen = form.getAll("components").map(String);
39 const result = await settle(
40 statusAdmin.scheduleMaintenance({
41 title: text(form, "title"),
42 message: String(form.get("message") ?? ""),
43 components: chosen,
Fast pages, required checks on the branch, self-hosted runners, honest incidents44 starts_at: utc(text(form, "starts_at"), zone) ?? "",
45 ends_at: utc(text(form, "ends_at"), zone) ?? "",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas46 notify: form.get("notify") === "on",
47 by: staff.email,
48 }),
49 );
50 if (!result.ok) return { error: result.error, values, chosen } satisfies ActionData;
51 if (!result.value.ok) return { error: result.value.error.message, values, chosen } satisfies ActionData;
52 return redirect(`/incidents/maintenance/${result.value.value.id}?done=scheduled`);
53}
54
55export default function ScheduleMaintenance({ loaderData, actionData }: Route.ComponentProps) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents56 const { components, error, start, end, zone } = loaderData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas57 const failed = actionData as ActionData | undefined;
58 const v = failed?.values ?? {};
59 return (
60 <main className="mx-auto max-w-3xl px-4 py-8 sm:py-10">
61 <BackLink to="/incidents?tab=maintenance">Incidents</BackLink>
62 <div className="mt-3">
63 <PageHeader
64 title="Schedule maintenance"
65 description="Shown on status.g1t.sh as upcoming maintenance at once; during the window its parts show “Under maintenance” and their checks do not count against uptime. It completes on its own when the window ends."
66 />
67 </div>
68 <div className="mt-5 space-y-3">
69 {error && <Notice tone="warn">The status worker did not answer: {error}</Notice>}
70 {failed && <Notice tone="error">{failed.error}</Notice>}
71 </div>
72 <form method="post" className="mt-6 space-y-6">
73 <Section title="What and when">
74 <div className="grid gap-4 sm:grid-cols-2">
75 <Field label="Title" className="sm:col-span-2">
76 <Input name="title" required maxLength={120} autoFocus defaultValue={v.title} placeholder="Database upgrade" />
77 </Field>
Fast pages, required checks on the branch, self-hosted runners, honest incidents78 <Field label={`Starts (${zone})`}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas79 <Input type="datetime-local" name="starts_at" required defaultValue={v.starts_at || start} />
80 </Field>
Fast pages, required checks on the branch, self-hosted runners, honest incidents81 <Field label={`Ends (${zone})`} hint="Up to 72 hours after it starts.">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas82 <Input type="datetime-local" name="ends_at" required defaultValue={v.ends_at || end} />
83 </Field>
84 </div>
85 </Section>
86 <Section title="Parts affected">
87 <div className="flex flex-wrap gap-2">
88 {components.map((c) => (
89 <label
90 key={c.key}
91 className="inline-flex cursor-pointer items-center gap-2 rounded-md border border-line px-2.5 py-1.5 text-sm has-checked:border-merged/60 has-checked:bg-merged/10"
92 >
93 <input type="checkbox" name="components" value={c.key} defaultChecked={failed?.chosen.includes(c.key)} className="accent-[#b6a8ff]" />
94 {c.name}
95 </label>
96 ))}
97 </div>
98 </Section>
99 <Section title="Message" description="What will happen and what people will notice. Shown publicly.">
100 <Textarea name="message" rows={4} required maxLength={4000} aria-label="Message" defaultValue={v.message} placeholder="We are upgrading the database behind git. Pushes may pause for up to five minutes; clones and the website keep working." />
101 <label className="mt-3 flex items-center gap-2 text-sm">
102 <input type="checkbox" name="notify" defaultChecked className="accent-[#b6a8ff]" />
103 Email subscribers now, when it starts, and when it ends
104 </label>
105 </Section>
106 <Button type="submit" variant="lavender">
107 Schedule
108 </Button>
109 </form>
110 </main>
111 );
112}