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

164 lines7,367 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 { ExternalLink } from "lucide-react";
2import { redirect } from "react-router";
3
4import type { MaintenanceChange } from "@g1t/contracts";
5
6import type { Route } from "./+types/maintenance";
7import { BackLink, MaintenanceBadge } from "~/components/incidents";
8import { Button, EmptyState, Field, Notice, PageHeader, Section, Textarea, When } from "~/components/ui";
9import { text } from "~/lib/forms";
10import { duration, secondsBetween } from "~/lib/incidents";
11import { statusAdmin } from "~/lib/services.server";
12import { settle } from "~/lib/settle";
13import { requireStaff } from "~/lib/staff";
14
15export const meta: Route.MetaFunction = ({ loaderData }) => [
16 { title: `${loaderData?.maintenance?.title ?? "Maintenance"} · sudo` },
17 { name: "robots", content: "noindex, nofollow" },
18];
19
20const DONE: Record<string, string> = {
21 scheduled: "Scheduled. status.g1t.sh lists it as upcoming within 30 seconds.",
22 update: "Update posted.",
23 start: "Started early.",
24 complete: "Marked complete.",
25 cancel: "Cancelled.",
26};
27
28export async function loader({ params, request, context }: Route.LoaderArgs) {
29 requireStaff(context);
30 const [board, components] = await Promise.all([settle(statusAdmin.board()), settle(statusAdmin.components())]);
31 const maintenance = board.ok ? (board.value.maintenance.find((m) => m.id === params.id) ?? null) : null;
32 if (board.ok && !maintenance) throw new Response("No such maintenance, or it ended over 90 days ago.", { status: 404 });
33 return {
34 maintenance,
35 names: components.ok ? components.value : [],
36 error: board.ok ? null : board.error,
37 done: DONE[new URL(request.url).searchParams.get("done") ?? ""] ?? null,
38 };
39}
40
41export async function action({ params, request, context }: Route.ActionArgs) {
42 const staff = requireStaff(context);
43 const form = await request.formData();
44 const act = text(form, "intent") as MaintenanceChange["action"];
45 const result = await settle(
46 statusAdmin.changeMaintenance(params.id, { action: act, message: String(form.get("message") ?? ""), notify: form.get("notify") === "on", by: staff.email }),
47 );
48 if (!result.ok) return { error: result.error };
49 if (!result.value.ok) return { error: result.value.error.message };
50 return redirect(`/incidents/maintenance/${params.id}?done=${act}`);
51}
52
53export default function Maintenance({ loaderData, actionData }: Route.ComponentProps) {
54 const { maintenance: m, names, error, done } = loaderData;
55 const failed = actionData as { error: string } | undefined;
56 if (!m) {
57 return (
58 <main className="mx-auto max-w-4xl px-4 py-8">
59 <Notice tone="warn">The status worker did not answer: {error}</Notice>
60 </main>
61 );
62 }
63 const name = new Map(names.map((c) => [c.key, c.name]));
64 const live = m.state === "scheduled" || m.state === "in_progress";
65 return (
66 <main className="mx-auto max-w-4xl px-4 py-8 sm:py-10">
67 <BackLink to="/incidents?tab=maintenance">Incidents</BackLink>
68 <div className="mt-3">
69 <PageHeader
70 title={m.title}
71 description={
72 <>
73 <MaintenanceBadge state={m.state} /> <When at={m.starts_at} time /> to <When at={m.ends_at} time /> · {duration(secondsBetween(m.starts_at, m.ends_at))} ·{" "}
74 {m.components.map((k) => name.get(k) ?? k).join(", ")}
75 </>
76 }
77 actions={
78 <a href={m.url} className="inline-flex items-center gap-1.5 text-sm text-merged hover:underline">
79 On the status page <ExternalLink size={13} aria-hidden="true" />
80 </a>
81 }
82 />
83 </div>
84 <div className="mt-4 space-y-3">
85 {done && <Notice tone="ok">{done}</Notice>}
86 {failed && <Notice tone="error">{failed.error}</Notice>}
87 </div>
88 <div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
89 <div className="min-w-0 space-y-6">
90 {live && (
91 <Section title="Post an update" description="Shown on its page and the status page.">
92 <form method="post" className="space-y-3">
93 <Textarea name="message" rows={3} maxLength={4000} aria-label="Update" placeholder="Running a little long: about 20 more minutes." />
94 <div className="flex flex-wrap items-center justify-between gap-3">
95 <label className="flex items-center gap-2 text-sm">
96 <input type="checkbox" name="notify" className="accent-[#b6a8ff]" />
97 Email subscribers
98 </label>
99 <Button type="submit" name="intent" value="update" variant="lavender">
100 Post update
101 </Button>
102 </div>
103 </form>
104 </Section>
105 )}
106 <Section title="Updates" description="Newest first, as the status page shows them.">
107 {m.updates.length === 0 ? (
108 <EmptyState title="No updates" />
109 ) : (
110 <ol className="space-y-3">
111 {m.updates.map((u) => (
112 <li key={u.id} className="rounded-md border border-line bg-raised/30 px-3.5 py-3">
113 <p className="text-xs text-faint">
114 <When at={u.at} time />
115 </p>
116 <p className="mt-1 text-sm whitespace-pre-line text-fg-soft">{u.text}</p>
117 </li>
118 ))}
119 </ol>
120 )}
121 </Section>
122 </div>
123 <aside className="min-w-0 space-y-6">
124 {live ? (
125 <Section title="Move it along" description="It starts and completes on its own with its window; do it by hand when the work runs early or late.">
126 <form method="post" className="space-y-3">
127 <Field label="Message (optional)">
128 <Textarea name="message" rows={2} maxLength={4000} placeholder="Said for you when empty." />
129 </Field>
130 <label className="flex items-center gap-2 text-sm">
131 <input type="checkbox" name="notify" defaultChecked className="accent-[#b6a8ff]" />
132 Email subscribers
133 </label>
134 <div className="flex flex-wrap gap-2">
135 {m.state === "scheduled" && (
136 <Button type="submit" name="intent" value="start" variant="quiet">
137 Start now
138 </Button>
139 )}
140 <Button type="submit" name="intent" value="complete" variant="quiet">
141 Complete
142 </Button>
143 <Button type="submit" name="intent" value="cancel" variant="danger">
144 Cancel
145 </Button>
146 </div>
147 </form>
148 </Section>
149 ) : (
150 <Section title="Finished">
151 <p className="text-sm text-muted">This maintenance is {m.state === "completed" ? "complete" : "cancelled"}. It stays in the status page's history.</p>
152 </Section>
153 )}
154 <Section title="Scheduled by">
155 <p className="font-mono text-xs text-fg-soft">{m.created_by}</p>
156 <p className="mt-1 text-xs text-faint">
157 <When at={m.created_at} time />
158 </p>
159 </Section>
160 </aside>
161 </div>
162 </main>
163 );
164}