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 | * A postmortem's starting point: the timeline written out from the | |
| 3 | * incident's own, the impact from its parts and times, and the follow-ups | |
| 4 | * as action items. Staff edit it before publishing. No Workers imports. | |
| 5 | */ | |
| 6 | import type { AdminIncident, FollowUp, PostmortemFields, TimelineEntry } from "@g1t/contracts"; | |
| 7 | ||
| 8 | import { duration } from "./incidents.ts"; | |
| 9 | import { IMPACT_WORD } from "./model.ts"; | |
| 10 | ||
| 11 | const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| 12 | ||
| 13 | /** "5 Oct 14:03 UTC". */ | |
| 14 | export function stamp(at: string): string { | |
| 15 | const d = new Date(at); | |
| 16 | const hh = String(d.getUTCHours()).padStart(2, "0"); | |
| 17 | const mm = String(d.getUTCMinutes()).padStart(2, "0"); | |
| 18 | return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${hh}:${mm} UTC`; | |
| 19 | } | |
| 20 | ||
| 21 | /** Lines not worth a reader's time: acknowledgements, follow-up ticks, the postmortem itself. */ | |
| 22 | const SKIP = new Set(["acknowledged", "followup", "postmortem"]); | |
| 23 | ||
| 24 | /** The internal timeline as a list, one line per entry, public updates marked. */ | |
| 25 | export function timelineText(entries: TimelineEntry[]): string { | |
| 26 | return entries | |
| 27 | .filter((e) => !SKIP.has(e.kind)) | |
| 28 | .map((e) => `- ${stamp(e.at)}: ${e.kind === "update" ? "Status page: " : ""}${e.text.replace(/\s*\n+\s*/g, " ")}`) | |
| 29 | .join("\n"); | |
| 30 | } | |
| 31 | ||
| 32 | export function postmortemDraft( | |
| 33 | incident: Pick<AdminIncident, "started_at" | "resolved_at" | "components" | "durations">, | |
| 34 | timeline: TimelineEntry[], | |
| 35 | followups: FollowUp[], | |
| 36 | names: Map<string, string>, | |
| 37 | ): PostmortemFields { | |
| 38 | const parts = incident.components | |
| 39 | .filter((c) => c.impact !== "operational") | |
| 40 | .map((c) => `${names.get(c.key) ?? c.key} (${IMPACT_WORD[c.impact].toLowerCase()})`); | |
| 41 | const lasted = incident.durations.to_resolve != null ? ` for ${duration(incident.durations.to_resolve)}` : ""; | |
| 42 | const window = `from ${stamp(incident.started_at)}${incident.resolved_at ? ` to ${stamp(incident.resolved_at)}` : ""}`; | |
| 43 | return { | |
| 44 | summary: "", | |
| 45 | impact: `${parts.join(", ") || "No part"} affected${lasted}, ${window}.`, | |
| 46 | timeline: timelineText(timeline), | |
| 47 | root_cause: "", | |
| 48 | went_well: "", | |
| 49 | went_badly: "", | |
| 50 | action_items: followups.map((f) => `- ${f.title}`).join("\n"), | |
| 51 | }; | |
| 52 | } |