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 | * The status page at status.g1t.sh (apps/status): what its JSON and feeds | |
| 3 | * say, and the staff-only entrypoint sudo runs incidents, maintenance and | |
| 4 | * postmortems through. Fields are snake_case, like the API. | |
| 5 | */ | |
| 6 | import type { Result } from "./result"; | |
| 7 | ||
| 8 | /** | |
| 9 | * One part's state. Its last check, made worse by an open incident's | |
| 10 | * impact on it, or `maintenance` during a maintenance window. | |
| 11 | * `unmonitored`: no check exists for it and nothing is reported. | |
| 12 | */ | |
| 13 | export type StatusComponentState = "up" | "degraded" | "partial" | "down" | "maintenance" | "unmonitored"; | |
| 14 | ||
| 15 | /** The whole of g1t, in one word. `unknown` until anything has been checked. */ | |
| 16 | export type StatusOverallState = "up" | "degraded" | "down" | "maintenance" | "unknown"; | |
| 17 | ||
| 18 | /** Where an incident stands, in the words status pages use. */ | |
| 19 | export type IncidentStatus = "investigating" | "identified" | "monitoring" | "resolved"; | |
| 20 | ||
| 21 | /** How bad an incident is, for the team: SEV1 is the worst. Never shown on the status page. */ | |
| 22 | export type IncidentSeverity = "sev1" | "sev2" | "sev3" | "sev4"; | |
| 23 | ||
| 24 | /** What an incident does to one part, while it is open. */ | |
| 25 | export type ComponentImpact = "operational" | "degraded" | "partial_outage" | "major_outage"; | |
| 26 | ||
| 27 | /** An incident's impact as a whole: some parts having trouble, or a major outage. */ | |
| 28 | export type IncidentImpact = "degraded" | "down"; | |
| 29 | ||
| 30 | export type StatusComponent = { | |
| 31 | key: string; | |
| 32 | name: string; | |
| 33 | /** Where people meet it: `api.g1t.sh`. */ | |
| 34 | address: string; | |
| 35 | /** What the check looks at, said plainly. */ | |
| 36 | checks: string; | |
| 37 | /** What the page shows: the check, an incident's impact (the worse wins), or maintenance. */ | |
| 38 | state: StatusComponentState; | |
| 39 | /** What the last check alone said. */ | |
| 40 | check_state: StatusComponentState; | |
| 41 | /** What was seen: "Answered in 84 ms", "Failed: HTTP 502". */ | |
| 42 | detail: string; | |
| 43 | latency_ms: number | null; | |
| 44 | /** Share of checks that answered over the last 90 days, 0 to 100. Null with no checks yet. */ | |
| 45 | uptime_90d: number | null; | |
| 46 | }; | |
| 47 | ||
| 48 | export type StatusIncidentUpdate = { | |
| 49 | id: string; | |
| 50 | at: string; | |
| 51 | status: IncidentStatus; | |
| 52 | text: string; | |
| 53 | }; | |
| 54 | ||
| 55 | export type StatusIncident = { | |
| 56 | id: string; | |
| 57 | title: string; | |
| 58 | /** `down` when any part has a major outage. */ | |
| 59 | impact: IncidentImpact; | |
| 60 | status: IncidentStatus; | |
| 61 | /** Keys of the parts it affects. */ | |
| 62 | components: string[]; | |
| 63 | /** What it does to each part. */ | |
| 64 | component_impacts: { key: string; impact: ComponentImpact }[]; | |
| 65 | started_at: string; | |
| 66 | /** Null while it is still going on. */ | |
| 67 | resolved_at: string | null; | |
| 68 | /** Its page: `https://status.g1t.sh/incidents/<id>`. */ | |
| 69 | url: string; | |
| 70 | /** When its postmortem was published on that page; null until then. */ | |
| 71 | postmortem_published_at: string | null; | |
| 72 | /** Newest first. */ | |
| 73 | updates: StatusIncidentUpdate[]; | |
| 74 | }; | |
| 75 | ||
| 76 | export type MaintenanceState = "scheduled" | "in_progress" | "completed" | "cancelled"; | |
| 77 | ||
| 78 | export type StatusMaintenanceUpdate = { id: string; at: string; text: string }; | |
| 79 | ||
| 80 | /** Planned work, announced ahead. Its parts show "Under maintenance" during the window. */ | |
| 81 | export type StatusMaintenance = { | |
| 82 | id: string; | |
| 83 | title: string; | |
| 84 | message: string; | |
| 85 | components: string[]; | |
| 86 | /** The window, RFC 3339, UTC. */ | |
| 87 | starts_at: string; | |
| 88 | ends_at: string; | |
| 89 | state: MaintenanceState; | |
| 90 | url: string; | |
| 91 | /** Newest first. */ | |
| 92 | updates: StatusMaintenanceUpdate[]; | |
| 93 | }; | |
| 94 | ||
| 95 | export type StatusOverall = { | |
| 96 | state: StatusOverallState; | |
| 97 | /** Two or three words: "All systems normal", "Partial outage", "Major outage". */ | |
| 98 | title: string; | |
| 99 | /** One sentence, naming what is affected. */ | |
| 100 | line: string; | |
| 101 | }; | |
| 102 | ||
| 103 | /** `GET status.g1t.sh/status.json`. */ | |
| 104 | export type StatusReport = { | |
| 105 | /** When the parts were last checked; null before the first check. */ | |
| 106 | checked_at: string | null; | |
| 107 | overall: StatusOverall; | |
| 108 | components: StatusComponent[]; | |
| 109 | /** Open incidents, and those resolved in the last 90 days, newest first. */ | |
| 110 | incidents: StatusIncident[]; | |
| 111 | /** Maintenance under way or still to come, soonest first. */ | |
| 112 | maintenance: StatusMaintenance[]; | |
| 113 | }; | |
| 114 | ||
| 115 | // --- Staff --------------------------------------------------------------------- | |
| 116 | ||
| 117 | /** `draft`: detected or saved, not on the page yet. `dismissed`: a draft closed as a false alarm. */ | |
| 118 | export type IncidentVisibility = "draft" | "public" | "dismissed"; | |
| 119 | ||
| 120 | /** What a line on an incident's timeline records. */ | |
| 121 | export type TimelineKind = | |
| 122 | | "declared" | |
| 123 | | "detected" | |
| 124 | | "published" | |
| 125 | | "dismissed" | |
| 126 | /** A public update: what the status page shows. */ | |
| 127 | | "update" | |
| 128 | /** A note only staff see. */ | |
| 129 | | "note" | |
| 130 | | "status" | |
| 131 | | "severity" | |
| 132 | | "impact" | |
| 133 | | "role" | |
| 134 | | "acknowledged" | |
| 135 | /** A check flipped back to working while the incident was open. */ | |
| 136 | | "recovered" | |
| 137 | /** A check kept failing while the incident was open. */ | |
| 138 | | "failing" | |
| 139 | | "followup" | |
| 140 | | "postmortem"; | |
| 141 | ||
| 142 | export type TimelineEntry = { | |
| 143 | id: string; | |
| 144 | at: string; | |
| 145 | /** A staff email, or `status` for what the checks wrote. */ | |
| 146 | by: string; | |
| 147 | kind: TimelineKind; | |
| 148 | /** Shown on the status page. */ | |
| 149 | public: boolean; | |
| 150 | /** For public updates: the status it was posted with. */ | |
| 151 | status: IncidentStatus | null; | |
| 152 | text: string; | |
| 153 | /** How many subscribers it was emailed to; null when it was not. */ | |
| 154 | notified: number | null; | |
| 155 | }; | |
| 156 | ||
| 157 | export type FollowUp = { | |
| 158 | id: string; | |
| 159 | title: string; | |
| 160 | owner: string | null; | |
| 161 | done_at: string | null; | |
| 162 | created_at: string; | |
| 163 | created_by: string; | |
| 164 | }; | |
| 165 | ||
| 166 | export type PostmortemFields = { | |
| 167 | summary: string; | |
| 168 | impact: string; | |
| 169 | timeline: string; | |
| 170 | root_cause: string; | |
| 171 | went_well: string; | |
| 172 | went_badly: string; | |
| 173 | /** What will change, one per line; filled in from the follow-ups. */ | |
| 174 | action_items: string; | |
| 175 | }; | |
| 176 | ||
| 177 | export type Postmortem = PostmortemFields & { | |
| 178 | updated_at: string; | |
| 179 | updated_by: string; | |
| 180 | published_at: string | null; | |
| 181 | }; | |
| 182 | ||
| 183 | /** Seconds from the impact starting to each milestone, once reached. */ | |
| 184 | export type IncidentDurations = { | |
| 185 | to_acknowledge: number | null; | |
| 186 | to_mitigate: number | null; | |
| 187 | to_resolve: number | null; | |
| 188 | }; | |
| 189 | ||
| 190 | export type AdminIncident = { | |
| 191 | id: string; | |
| 192 | title: string; | |
| 193 | severity: IncidentSeverity; | |
| 194 | status: IncidentStatus; | |
| 195 | visibility: IncidentVisibility; | |
| 196 | /** Declared by staff, or drafted when a check kept failing. */ | |
| 197 | source: "declared" | "detected"; | |
| 198 | components: { key: string; impact: ComponentImpact }[]; | |
| 199 | /** When the impact began (can be earlier than declared). */ | |
| 200 | started_at: string; | |
| 201 | declared_at: string; | |
| 202 | acknowledged_at: string | null; | |
| 203 | mitigated_at: string | null; | |
| 204 | resolved_at: string | null; | |
| 205 | published_at: string | null; | |
| 206 | commander: string | null; | |
| 207 | communications: string | null; | |
| 208 | created_by: string; | |
| 209 | postmortem_published_at: string | null; | |
| 210 | durations: IncidentDurations; | |
| 211 | /** Follow-ups open and done. */ | |
| 212 | followups_open: number; | |
| 213 | followups_done: number; | |
| 214 | /** The last public update's time, for "updated 12 minutes ago". */ | |
| 215 | last_update_at: string | null; | |
| 216 | }; | |
| 217 | ||
| 218 | export type AdminIncidentDetail = AdminIncident & { | |
| 219 | /** Oldest first: public updates, notes and what changed. */ | |
| 220 | timeline: TimelineEntry[]; | |
| 221 | followups: FollowUp[]; | |
| 222 | postmortem: Postmortem | null; | |
| 223 | /** What the postmortem editor starts from before anything is saved: the timeline filled in, follow-ups listed. */ | |
| 224 | postmortem_draft: PostmortemFields; | |
| 225 | /** Its page on the status site. */ | |
| 226 | url: string; | |
| 227 | }; | |
| 228 | ||
| 229 | export type AdminMaintenance = StatusMaintenance & { created_at: string; created_by: string }; | |
| 230 | ||
| 231 | /** One line of the status worker's audit log: every staff change, with who made it. */ | |
| 232 | export type StatusAuditEntry = { id: string; at: string; by: string; action: string; target: string; detail: string }; | |
| 233 | ||
| 234 | export type ImpactInput = { key: string; impact: ComponentImpact }; | |
| 235 | ||
| 236 | export type DeclareIncident = { | |
| 237 | title: string; | |
| 238 | severity: IncidentSeverity; | |
| 239 | status?: IncidentStatus; | |
| 240 | components: ImpactInput[]; | |
| 241 | /** The first public update. */ | |
| 242 | message: string; | |
| 243 | /** When the impact began, if earlier than now. RFC 3339. */ | |
| 244 | started_at?: string | null; | |
| 245 | commander?: string | null; | |
| 246 | communications?: string | null; | |
| 247 | /** Email subscribers about it. */ | |
| 248 | notify: boolean; | |
| 249 | /** The staff member's email, kept with every line. */ | |
| 250 | by: string; | |
| 251 | }; | |
| 252 | ||
| 253 | /** An update, a note, or a change, posted together. */ | |
| 254 | export type IncidentChange = { | |
| 255 | /** Shown on the status page (and emailed when `notify`), or a note only staff see. */ | |
| 256 | public: boolean; | |
| 257 | message: string; | |
| 258 | status?: IncidentStatus | null; | |
| 259 | severity?: IncidentSeverity | null; | |
| 260 | /** The parts' impact from now on; parts left out keep theirs. */ | |
| 261 | impacts?: ImpactInput[] | null; | |
| 262 | notify?: boolean; | |
| 263 | by: string; | |
| 264 | }; | |
| 265 | ||
| 266 | export type RolesChange = { commander: string | null; communications: string | null; by: string }; | |
| 267 | ||
| 268 | /** Putting a draft on the status page, with its first public update. */ | |
| 269 | export type PublishIncident = { title?: string | null; message: string; notify: boolean; by: string }; | |
| 270 | ||
| 271 | export type NewMaintenance = { | |
| 272 | title: string; | |
| 273 | message: string; | |
| 274 | components: string[]; | |
| 275 | starts_at: string; | |
| 276 | ends_at: string; | |
| 277 | notify: boolean; | |
| 278 | by: string; | |
| 279 | }; | |
| 280 | ||
| 281 | export type MaintenanceChange = { | |
| 282 | /** `update` posts a message; the others also move it along. */ | |
| 283 | action: "update" | "start" | "complete" | "cancel"; | |
| 284 | message: string; | |
| 285 | notify: boolean; | |
| 286 | by: string; | |
| 287 | }; | |
| 288 | ||
| 289 | export type StatusBoard = { | |
| 290 | /** Open and draft incidents, and those resolved in the last 180 days, newest first. */ | |
| 291 | incidents: AdminIncident[]; | |
| 292 | /** Upcoming, under way, and finished in the last 90 days. */ | |
| 293 | maintenance: AdminMaintenance[]; | |
| 294 | /** Confirmed email subscribers. */ | |
| 295 | subscribers: number; | |
| 296 | /** Whether the status worker can send email (a sender and a token secret). */ | |
| 297 | email: boolean; | |
| 298 | }; | |
| 299 | ||
| 300 | /** | |
| 301 | * The status worker's `StatusAdmin` entrypoint. Only a service binding | |
| 302 | * reaches it (sudo's `STATUS`); status.g1t.sh itself has no way to write. | |
| 303 | * Every change is kept in its audit log with `by`. | |
| 304 | */ | |
| 305 | export interface StatusAdminApi { | |
| 306 | /** The parts the page lists, for choosing which an incident affects. */ | |
| 307 | components(): Promise<{ key: string; name: string }[]>; | |
| 308 | board(): Promise<StatusBoard>; | |
| 309 | incident(id: string): Promise<AdminIncidentDetail | null>; | |
| 310 | /** Open incidents, drafts included: the sidebar's count. */ | |
| 311 | openCount(): Promise<number>; | |
| 312 | declare(input: DeclareIncident): Promise<Result<AdminIncident>>; | |
| 313 | update(id: string, change: IncidentChange): Promise<Result<AdminIncident>>; | |
| 314 | roles(id: string, change: RolesChange): Promise<Result<AdminIncident>>; | |
| 315 | publish(id: string, input: PublishIncident): Promise<Result<AdminIncident>>; | |
| 316 | dismiss(id: string, input: { reason: string; by: string }): Promise<Result<AdminIncident>>; | |
| 317 | addFollowUp(id: string, input: { title: string; owner: string | null; by: string }): Promise<Result<FollowUp>>; | |
| 318 | setFollowUp(id: string, followUp: string, input: { done: boolean; by: string }): Promise<Result<FollowUp>>; | |
| 319 | savePostmortem(id: string, input: PostmortemFields & { by: string }): Promise<Result<Postmortem>>; | |
| 320 | publishPostmortem(id: string, input: { publish: boolean; by: string }): Promise<Result<Postmortem>>; | |
| 321 | scheduleMaintenance(input: NewMaintenance): Promise<Result<AdminMaintenance>>; | |
| 322 | changeMaintenance(id: string, change: MaintenanceChange): Promise<Result<AdminMaintenance>>; | |
| 323 | /** Newest first, 100 at a time, before `before`. */ | |
| 324 | audit(filter?: { before?: string | null }): Promise<StatusAuditEntry[]>; | |
| 325 | } | |
| 326 | ||
| 327 | // --- Words both sides use ------------------------------------------------------------ | |
| 328 | ||
| 329 | /** What each severity means, for the team. Never shown on the status page. */ | |
| 330 | export const INCIDENT_SEVERITIES: { value: IncidentSeverity; label: string; about: string; notify: boolean }[] = [ | |
| 331 | { | |
| 332 | value: "sev1", | |
| 333 | label: "SEV1", | |
| 334 | about: "Critical: g1t is down or unusable for most people, or data is at risk. Everyone on it; a public update at least every 30 minutes.", | |
| 335 | notify: true, | |
| 336 | }, | |
| 337 | { | |
| 338 | value: "sev2", | |
| 339 | label: "SEV2", | |
| 340 | about: "Major: a core part (sign-in, git, the API, agents) is broken or badly degraded for many people. A public update at least hourly.", | |
| 341 | notify: true, | |
| 342 | }, | |
| 343 | { value: "sev3", label: "SEV3", about: "Minor: one part is degraded or broken for some people, and there is a way around it.", notify: false }, | |
| 344 | { value: "sev4", label: "SEV4", about: "Low: little or no customer impact, such as a cosmetic fault or a risk caught before anyone noticed.", notify: false }, | |
| 345 | ]; | |
| 346 | ||
| 347 | export const INCIDENT_STATUSES: { value: IncidentStatus; label: string; about: string }[] = [ | |
| 348 | { value: "investigating", label: "Investigating", about: "Something is wrong; the cause is not known yet." }, | |
| 349 | { value: "identified", label: "Identified", about: "The cause is known and a fix is under way." }, | |
| 350 | { value: "monitoring", label: "Monitoring", about: "A fix is out; watching that it holds." }, | |
| 351 | { value: "resolved", label: "Resolved", about: "Over. Closes the incident." }, | |
| 352 | ]; | |
| 353 | ||
| 354 | export const COMPONENT_IMPACTS: { value: ComponentImpact; label: string }[] = [ | |
| 355 | { value: "operational", label: "Operational" }, | |
| 356 | { value: "degraded", label: "Degraded performance" }, | |
| 357 | { value: "partial_outage", label: "Partial outage" }, | |
| 358 | { value: "major_outage", label: "Major outage" }, | |
| 359 | ]; |