flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/status/src/postmortem.ts

52 lines2,216 bytesCodeBlame
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 */
6import type { AdminIncident, FollowUp, PostmortemFields, TimelineEntry } from "@g1t/contracts";
7
8import { duration } from "./incidents.ts";
9import { IMPACT_WORD } from "./model.ts";
10
11const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
12
13/** "5 Oct 14:03 UTC". */
14export 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. */
22const SKIP = new Set(["acknowledged", "followup", "postmortem"]);
23
24/** The internal timeline as a list, one line per entry, public updates marked. */
25export 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
32export 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}