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 quotas | 1 | /** |
| 2 | * Email from the status page: subscription confirmations, incident and | |
| 3 | * maintenance updates to subscribers, and the staff alert when a draft is | |
| 4 | * detected. Sending goes through a `Sender`, so the page works the same | |
| 5 | * with no email at all (an installation without it, or tests): the feeds | |
| 6 | * still carry every update. On g1t.sh the sender is Cloudflare Email | |
| 7 | * Sending, the `send_email` binding identity uses too. No Workers imports. | |
| 8 | */ | |
| 9 | ||
| 10 | export type Mail = { to: string; subject: string; text: string; html: string; headers?: Record<string, string> }; | |
| 11 | ||
| 12 | export interface Sender { | |
| 13 | send(mail: Mail): Promise<void>; | |
| 14 | } | |
| 15 | ||
| 16 | /** The `send_email` binding, as Cloudflare Email Sending gives it. */ | |
| 17 | export type EmailBinding = { | |
| 18 | send(message: { to: string; from: string; subject: string; text: string; html: string; headers?: Record<string, string> }): Promise<unknown>; | |
| 19 | }; | |
| 20 | ||
| 21 | export const DEFAULT_FROM = "g1t status <noreply@g1t.sh>"; | |
| 22 | ||
| 23 | /** A sender over the binding, or null when there is none. */ | |
| 24 | export function bindingSender(binding: EmailBinding | undefined, from = DEFAULT_FROM): Sender | null { | |
| 25 | if (!binding || typeof binding.send !== "function") return null; | |
| 26 | return { send: async ({ to, subject, text, html, headers }) => void (await binding.send({ to, from, subject, text, html, ...(headers ? { headers } : {}) })) }; | |
| 27 | } | |
| 28 | ||
| 29 | /** Collects mail instead of sending it: for tests and previews. */ | |
| 30 | export function memorySender(): Sender & { sent: Mail[] } { | |
| 31 | const sent: Mail[] = []; | |
| 32 | return { sent, send: async (mail) => void sent.push(mail) }; | |
| 33 | } | |
| 34 | ||
| 35 | function escape(text: string): string { | |
| 36 | return text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!); | |
| 37 | } | |
| 38 | ||
| 39 | export type Letter = { | |
| 40 | /** A line over the body, such as "Identified · Pushes failing". */ | |
| 41 | heading: string; | |
| 42 | paragraphs: string[]; | |
| 43 | action?: { label: string; url: string }; | |
| 44 | footer: string[]; | |
| 45 | /** Where the reader leaves, as a link in the footer. */ | |
| 46 | unsubscribe?: string; | |
| 47 | }; | |
| 48 | ||
| 49 | /** The plain text and HTML of a letter; everything is escaped. */ | |
| 50 | export function render(letter: Letter): { text: string; html: string } { | |
| 51 | const text: string[] = [letter.heading, ""]; | |
| 52 | let html = `<div style="font-family:system-ui,sans-serif;max-width:520px;margin:0 auto;padding:32px 16px;color:#16150f">`; | |
| 53 | html += `<p style="margin:0 0 6px;font-size:13px;color:#6e6a5e">g1t status</p>`; | |
| 54 | html += `<h1 style="margin:0 0 16px;font-size:19px;line-height:1.35">${escape(letter.heading)}</h1>`; | |
| 55 | for (const p of letter.paragraphs) { | |
| 56 | text.push(p, ""); | |
| 57 | html += `<p style="font-size:15px;line-height:1.6;white-space:pre-line">${escape(p)}</p>`; | |
| 58 | } | |
| 59 | if (letter.action) { | |
| 60 | text.push(`${letter.action.label}: ${letter.action.url}`, ""); | |
| 61 | html += `<p style="margin:24px 0"><a href="${escape(letter.action.url)}" style="background:#16150f;color:#fff;text-decoration:none;padding:10px 18px;border-radius:6px;font-size:15px">${escape(letter.action.label)}</a></p>`; | |
| 62 | } | |
| 63 | for (const line of letter.footer) { | |
| 64 | text.push(line); | |
| 65 | html += `<p style="font-size:13px;line-height:1.6;color:#6e6a5e;margin:4px 0">${escape(line)}</p>`; | |
| 66 | } | |
| 67 | if (letter.unsubscribe) { | |
| 68 | text.push(`Unsubscribe: ${letter.unsubscribe}`); | |
| 69 | html += `<p style="font-size:13px;line-height:1.6;color:#6e6a5e;margin:4px 0"><a href="${escape(letter.unsubscribe)}" style="color:#6e6a5e">Unsubscribe</a></p>`; | |
| 70 | } | |
| 71 | html += "</div>"; | |
| 72 | return { text: `${text.join("\n").trim()}\n`, html }; | |
| 73 | } | |
| 74 | ||
| 75 | /** The headers that let mail apps offer one-click unsubscribing (RFC 8058). */ | |
| 76 | export function unsubscribeHeaders(url: string): Record<string, string> { | |
| 77 | return { "List-Unsubscribe": `<${url}>`, "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" }; | |
| 78 | } | |
| 79 | ||
| 80 | export function confirmLetter(link: string, parts: string[] | null): Letter { | |
| 81 | return { | |
| 82 | heading: "Confirm your subscription to g1t status", | |
| 83 | paragraphs: [ | |
| 84 | `Someone, hopefully you, asked to get an email when g1t posts an incident or planned maintenance${parts ? ` affecting ${parts.join(", ")}` : ""}. Confirm to start.`, | |
| 85 | ], | |
| 86 | action: { label: "Confirm subscription", url: link }, | |
| 87 | footer: ["The link works for 24 hours. If you did not ask for this, ignore this email and nothing happens."], | |
| 88 | }; | |
| 89 | } | |
| 90 | ||
| 91 | export function updateLetter(input: { heading: string; text: string; url: string; affects: string[]; unsubscribe: string }): Letter { | |
| 92 | return { | |
| 93 | heading: input.heading, | |
| 94 | paragraphs: [input.text, ...(input.affects.length ? [`Affects: ${input.affects.join(", ")}.`] : [])], | |
| 95 | action: { label: "See it on the status page", url: input.url }, | |
| 96 | footer: ["You get these because you subscribed at status.g1t.sh."], | |
| 97 | unsubscribe: input.unsubscribe, | |
| 98 | }; | |
| 99 | } | |
| 100 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 101 | /** |
| 102 | * The staff alert for a new detected draft. `lines` say what each part | |
| 103 | * did, slow and not answering told apart (detect.ts `troubleSentence`). | |
| 104 | */ | |
| 105 | export function alertLetter(input: { title: string; lines: string[]; link: string; note?: string }): Letter { | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 106 | return { |
| 107 | heading: input.title, | |
| 108 | paragraphs: [ | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 109 | input.lines.join("\n"), |
| 110 | ...(input.note ? [input.note] : []), | |
| 111 | "A draft incident is waiting in sudo. It is not on the status page until someone publishes it.", | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 112 | ], |
| 113 | action: { label: "Open it in sudo", url: input.link }, | |
| 114 | footer: ["Sent by status.g1t.sh to the staff alert address (STATUS_ALERT_EMAIL)."], | |
| 115 | }; | |
| 116 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 117 | |
| 118 | /** The follow-up when a detected draft recovered and was dismissed on its own. */ | |
| 119 | export function recoveredLetter(input: { title: string; text: string; link: string }): Letter { | |
| 120 | return { | |
| 121 | heading: `Recovered: ${input.title.replace(/^Detected: /, "")} — dismissed automatically`, | |
| 122 | paragraphs: [input.text, "Open it in sudo to read its timeline, or to declare an incident anyway."], | |
| 123 | action: { label: "Open it in sudo", url: input.link }, | |
| 124 | footer: ["Sent by status.g1t.sh to the staff alert address (STATUS_ALERT_EMAIL)."], | |
| 125 | }; | |
| 126 | } |