Skip to content
951 linesCodeBlameRaw

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 quotas1/**
2 * The status page's D1 database: each part's last check and daily tally
3 * (migrations/0001), and incidents, maintenance, postmortems, subscribers,
4 * detection and the audit log (migrations/0002).
5 */
6import type {
7 AdminIncident,
8 AdminIncidentDetail,
9 AdminMaintenance,
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders10 CheckHistory,
11 CheckSample,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12 ComponentImpact,
13 FollowUp,
14 ImpactInput,
15 IncidentSeverity,
16 IncidentStatus,
17 IncidentVisibility,
18 MaintenanceState,
19 Postmortem,
20 PostmortemFields,
21 StatusAuditEntry,
22 StatusIncident,
23 StatusMaintenance,
24 TimelineEntry,
25 TimelineKind,
26} from "@g1t/contracts";
27
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders28import { type DeployWindow, type Streak, type WatchedDraft, upgradeStreak } from "./detect.ts";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29import { type Entry, type IncidentFacts, durations, shortId } from "./incidents.ts";
30import { type Current, type DayRow, HISTORY_DAYS, dayOf, overallImpact } from "./model.ts";
31import { postmortemDraft } from "./postmortem.ts";
32import { parseParts, wants } from "./subscribers.ts";
33
34const DAY_MS = 24 * 60 * 60 * 1000;
35const iso = (ms: number) => new Date(ms).toISOString();
36
37/** One part's check, ready to keep. */
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders38export type Observation = Current & {
39 component: string;
40 /** The data centre the check ran from (cf-ray), when known. */
41 colo?: string | null;
42 /** A slow answer asked again (probe.ts `confirmSlow`): the first try's time. */
43 first_ms?: number | null;
44};
45
46/** How long every single check is kept (`check_history`), for sudo's incident pages. */
47export const CHECK_HISTORY_DAYS = 7;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas48
49/**
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders50 * Keeps one round of checks: each part's state, today's tally, every
51 * check for CHECK_HISTORY_DAYS, and the time. Parts under maintenance keep
52 * their last check but are left out of the tally: failures in a planned
53 * window do not count against uptime.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas54 */
55export async function record(db: D1Database, observations: Observation[], at: Date, maintenance: Set<string> = new Set()): Promise<void> {
56 const when = at.toISOString();
57 const day = dayOf(at);
58 const oldest = dayOf(at.getTime() - HISTORY_DAYS * DAY_MS);
59 const statements: D1PreparedStatement[] = [];
60 for (const o of observations) {
61 statements.push(
62 db
63 .prepare(
64 `INSERT INTO current (component, state, detail, latency_ms, checked_at) VALUES (?1, ?2, ?3, ?4, ?5)
65 ON CONFLICT (component) DO UPDATE SET state = ?2, detail = ?3, latency_ms = ?4, checked_at = ?5`,
66 )
67 .bind(o.component, o.state, o.detail, o.latency_ms, when),
68 );
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders69 if (o.state === "unmonitored") continue;
70 statements.push(
71 db
72 .prepare(`INSERT OR REPLACE INTO check_history (component, at, ms, outcome, colo, first_ms) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
73 .bind(o.component, when, o.latency_ms, o.state, o.colo ?? null, o.first_ms ?? null),
74 );
75 if (maintenance.has(o.component)) continue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas76 const up = o.state === "up" ? 1 : 0;
77 const degraded = o.state === "degraded" ? 1 : 0;
78 const down = o.state === "down" ? 1 : 0;
79 // A latency only counts when it answered: a timeout is not a speed.
80 const latency = o.state !== "down" && o.latency_ms != null ? o.latency_ms : 0;
81 const counted = o.state !== "down" && o.latency_ms != null ? 1 : 0;
82 statements.push(
83 db
84 .prepare(
85 `INSERT INTO daily (component, day, checks, up, degraded, down, latency_total, latency_count)
86 VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7)
87 ON CONFLICT (component, day) DO UPDATE SET
88 checks = checks + 1, up = up + ?3, degraded = degraded + ?4, down = down + ?5,
89 latency_total = latency_total + ?6, latency_count = latency_count + ?7`,
90 )
91 .bind(o.component, day, up, degraded, down, latency, counted),
92 );
93 }
94 statements.push(
95 db.prepare(`INSERT INTO meta (key, value) VALUES ('checked_at', ?1) ON CONFLICT (key) DO UPDATE SET value = ?1`).bind(when),
96 );
97 statements.push(db.prepare(`DELETE FROM daily WHERE day < ?1`).bind(oldest));
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders98 statements.push(db.prepare(`DELETE FROM check_history WHERE at < ?1`).bind(iso(at.getTime() - CHECK_HISTORY_DAYS * DAY_MS)));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas99 await db.batch(statements);
100}
101
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders102/** Every check of `components` from `from` to `to`, oldest first. */
103export async function checkHistory(db: D1Database, components: string[], from: Date, to: Date): Promise<CheckSample[]> {
104 if (!components.length) return [];
105 const list = rows<CheckSample>(
106 (await db
107 .prepare(
108 `SELECT component, at, ms, outcome, colo, first_ms FROM check_history
109 WHERE at >= ? AND at <= ? AND component IN (${inList(components)}) ORDER BY at, component`,
110 )
111 .bind(from.toISOString(), to.toISOString(), ...components)
112 .all()) as D1Result,
113 );
114 return list.map((r) => ({ ...r, ms: r.ms == null ? null : Number(r.ms), first_ms: r.first_ms == null ? null : Number(r.first_ms) }));
115}
116
117/** Before the impact began, and after it ended, the incident page shows this much more. */
118const AROUND_MS = 30 * 60_000;
119/** The most of one incident's checks the incident page shows: the last day of it. */
120const SPAN_MS = DAY_MS;
121
122/** The span of checks an incident's page shows: from before it began to after it ended (or now), at most a day, within what is kept. */
123export function historySpan(started_at: string, resolved_at: string | null, now: Date): { from: Date; to: Date } {
124 const to = Math.min(now.getTime(), resolved_at ? Date.parse(resolved_at) + AROUND_MS : now.getTime());
125 const from = Math.max(Date.parse(started_at) - AROUND_MS, to - SPAN_MS, now.getTime() - CHECK_HISTORY_DAYS * DAY_MS);
126 return { from: new Date(Math.min(from, to)), to: new Date(to) };
127}
128
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas129// --- Rows ------------------------------------------------------------------------
130
131type IncidentRow = {
132 id: string;
133 title: string;
134 severity: IncidentSeverity;
135 status: IncidentStatus;
136 visibility: IncidentVisibility;
137 source: "declared" | "detected";
138 started_at: string;
139 declared_at: string;
140 acknowledged_at: string | null;
141 mitigated_at: string | null;
142 resolved_at: string | null;
143 published_at: string | null;
144 commander: string | null;
145 communications: string | null;
146 created_by: string;
147};
148type ComponentRow = { incident_id: string; component: string; impact: ComponentImpact };
149type TimelineRow = {
150 id: string;
151 incident_id: string;
152 at: string;
153 by: string;
154 kind: TimelineKind;
155 public: number;
156 status: IncidentStatus | null;
157 text: string;
158 notified: number | null;
159};
160type FollowUpRow = FollowUp & { incident_id: string };
161type PostmortemRow = PostmortemFields & { incident_id: string; updated_at: string; updated_by: string; published_at: string | null };
162type MaintenanceRow = {
163 id: string;
164 title: string;
165 message: string;
166 components: string;
167 starts_at: string;
168 ends_at: string;
169 state: MaintenanceState;
170 notify: number;
171 created_at: string;
172 created_by: string;
173};
174type MaintenanceUpdateRow = { id: string; maintenance_id: string; at: string; by: string; text: string; notified: number | null };
175
176function keys(json: string): string[] {
177 try {
178 const value = JSON.parse(json);
179 return Array.isArray(value) ? value.map(String) : [];
180 } catch {
181 return [];
182 }
183}
184
185const rows = <T>(result: D1Result | undefined) => (result?.results ?? []) as T[];
186
187/** `in (?, ?, ?)` for a list, as bind arguments. */
188function inList(ids: string[]): string {
189 return ids.map(() => "?").join(", ") || "NULL";
190}
191
192export function incidentUrl(origin: string, id: string): string {
193 return `${origin}/incidents/${id}`;
194}
195
196export function maintenanceUrl(origin: string, id: string): string {
197 return `${origin}/maintenance/${id}`;
198}
199
200function components(id: string, all: ComponentRow[]): ImpactInput[] {
201 return all.filter((c) => c.incident_id === id).map((c) => ({ key: c.component, impact: c.impact }));
202}
203
204function toAdmin(row: IncidentRow, comps: ComponentRow[], timeline: TimelineRow[], followups: FollowUpRow[], pm: PostmortemRow | null): AdminIncident {
205 const mine = followups.filter((f) => f.incident_id === row.id);
206 const lastUpdate = timeline.filter((t) => t.incident_id === row.id && t.public === 1).reduce<string | null>((a, t) => (a && a > t.at ? a : t.at), null);
207 return {
208 id: row.id,
209 title: row.title,
210 severity: row.severity,
211 status: row.status,
212 visibility: row.visibility,
213 source: row.source,
214 components: components(row.id, comps),
215 started_at: row.started_at,
216 declared_at: row.declared_at,
217 acknowledged_at: row.acknowledged_at,
218 mitigated_at: row.mitigated_at,
219 resolved_at: row.resolved_at,
220 published_at: row.published_at,
221 commander: row.commander,
222 communications: row.communications,
223 created_by: row.created_by,
224 postmortem_published_at: pm?.published_at ?? null,
225 durations: durations(row),
226 followups_open: mine.filter((f) => !f.done_at).length,
227 followups_done: mine.filter((f) => f.done_at).length,
228 last_update_at: lastUpdate,
229 };
230}
231
232function toPublic(row: IncidentRow, comps: ComponentRow[], timeline: TimelineRow[], origin: string, pmPublished: string | null): StatusIncident {
233 const impacts = components(row.id, comps).filter((c) => c.impact !== "operational");
234 return {
235 id: row.id,
236 title: row.title,
237 impact: overallImpact(impacts),
238 status: row.status,
239 components: impacts.map((c) => c.key),
240 component_impacts: impacts,
241 started_at: row.started_at,
242 resolved_at: row.resolved_at,
243 url: incidentUrl(origin, row.id),
244 postmortem_published_at: pmPublished,
245 updates: timeline
246 .filter((t) => t.incident_id === row.id && t.public === 1)
247 .sort((a, b) => b.at.localeCompare(a.at))
248 .map((t) => ({ id: t.id, at: t.at, status: t.status ?? row.status, text: t.text })),
249 };
250}
251
252function toTimeline(t: TimelineRow): TimelineEntry {
253 return { id: t.id, at: t.at, by: t.by, kind: t.kind, public: t.public === 1, status: t.status, text: t.text, notified: t.notified };
254}
255
256function toMaintenance(row: MaintenanceRow, updates: MaintenanceUpdateRow[], origin: string): AdminMaintenance {
257 return {
258 id: row.id,
259 title: row.title,
260 message: row.message,
261 components: keys(row.components),
262 starts_at: row.starts_at,
263 ends_at: row.ends_at,
264 state: row.state,
265 url: maintenanceUrl(origin, row.id),
266 updates: updates
267 .filter((u) => u.maintenance_id === row.id)
268 .sort((a, b) => b.at.localeCompare(a.at))
269 .map((u) => ({ id: u.id, at: u.at, text: u.text })),
270 created_at: row.created_at,
271 created_by: row.created_by,
272 };
273}
274
275/** The public view of maintenance: no staff emails. */
276export function publicMaintenance(m: AdminMaintenance): StatusMaintenance {
277 const { created_at: _a, created_by: _b, ...rest } = m;
278 return rest;
279}
280
281// --- Public reads ------------------------------------------------------------------
282
283/** Public incidents matching `where`, with their parts and public updates. */
284async function publicIncidents(db: D1Database, where: string, args: unknown[], origin: string): Promise<StatusIncident[]> {
285 const list = rows<IncidentRow>(
286 (await db.prepare(`SELECT * FROM incident WHERE visibility = 'public' AND (${where}) ORDER BY started_at DESC LIMIT 500`).bind(...args).all()) as D1Result,
287 );
288 if (!list.length) return [];
289 const ids = list.map((i) => i.id);
290 const [comps, timeline, pms] = await db.batch([
291 db.prepare(`SELECT * FROM incident_component WHERE incident_id IN (${inList(ids)})`).bind(...ids),
292 db.prepare(`SELECT * FROM incident_timeline WHERE public = 1 AND incident_id IN (${inList(ids)})`).bind(...ids),
293 db.prepare(`SELECT incident_id, published_at FROM postmortem WHERE published_at IS NOT NULL AND incident_id IN (${inList(ids)})`).bind(...ids),
294 ]);
295 const published = new Map(rows<{ incident_id: string; published_at: string }>(pms).map((p) => [p.incident_id, p.published_at]));
296 return list.map((row) => toPublic(row, rows(comps), rows(timeline), origin, published.get(row.id) ?? null));
297}
298
299async function maintenanceWhere(db: D1Database, where: string, args: unknown[], origin: string): Promise<AdminMaintenance[]> {
300 const list = rows<MaintenanceRow>((await db.prepare(`SELECT * FROM maintenance WHERE ${where} ORDER BY starts_at DESC LIMIT 500`).bind(...args).all()) as D1Result);
301 if (!list.length) return [];
302 const ids = list.map((m) => m.id);
303 const updates = rows<MaintenanceUpdateRow>(
304 (await db.prepare(`SELECT * FROM maintenance_update WHERE maintenance_id IN (${inList(ids)})`).bind(...ids).all()) as D1Result,
305 );
306 return list.map((m) => toMaintenance(m, updates, origin));
307}
308
309/** Everything the front page reads. */
310export async function load(db: D1Database, now: Date, origin: string) {
311 const since = dayOf(now.getTime() - HISTORY_DAYS * DAY_MS);
312 const [current, meta, days] = await db.batch([
313 db.prepare(`SELECT component, state, detail, latency_ms FROM current`),
314 db.prepare(`SELECT value FROM meta WHERE key = 'checked_at'`),
315 db.prepare(`SELECT * FROM daily WHERE day >= ?1`).bind(since),
316 ]);
317 const [incidents, maintenance] = await Promise.all([
318 publicIncidents(db, `resolved_at IS NULL OR resolved_at >= ?1`, [iso(now.getTime() - HISTORY_DAYS * DAY_MS)], origin),
319 maintenanceWhere(db, `state IN ('scheduled', 'in_progress')`, [], origin),
320 ]);
321 const currentRows = rows<Current & { component: string }>(current);
322 return {
323 current: new Map(currentRows.map((row) => [row.component, { state: row.state, detail: row.detail, latency_ms: row.latency_ms }])),
324 checkedAt: (rows<{ value?: string }>(meta)[0]?.value ?? null) as string | null,
325 days: rows<DayRow>(days),
326 incidents,
327 maintenance: maintenance.map(publicMaintenance),
328 };
329}
330
331/** One public incident, with its postmortem once published. */
332export async function loadPublicIncident(db: D1Database, id: string, origin: string) {
333 const [incident] = await publicIncidents(db, `id = ?1`, [id], origin);
334 if (!incident) return null;
335 const pm = (await db.prepare(`SELECT * FROM postmortem WHERE incident_id = ?1 AND published_at IS NOT NULL`).bind(id).first()) as PostmortemRow | null;
336 return { incident, postmortem: pm };
337}
338
339export async function loadPublicMaintenance(db: D1Database, id: string, origin: string): Promise<StatusMaintenance | null> {
340 const [m] = await maintenanceWhere(db, `id = ?1`, [id], origin);
341 return m ? publicMaintenance(m) : null;
342}
343
344/** Incidents and finished maintenance since `since`, for the history page and the feeds. */
345export async function loadHistory(db: D1Database, since: Date, origin: string) {
346 const at = since.toISOString();
347 const [incidents, maintenance] = await Promise.all([
348 publicIncidents(db, `started_at >= ?1 OR resolved_at IS NULL`, [at], origin),
349 maintenanceWhere(db, `starts_at >= ?1 OR state IN ('scheduled', 'in_progress')`, [at], origin),
350 ]);
351 return { incidents, maintenance: maintenance.map(publicMaintenance) };
352}
353
354// --- Staff reads -------------------------------------------------------------------
355
356async function adminIncidents(db: D1Database, where: string, args: unknown[]): Promise<{ list: IncidentRow[]; comps: ComponentRow[]; timeline: TimelineRow[]; followups: FollowUpRow[]; pms: PostmortemRow[] }> {
357 const list = rows<IncidentRow>((await db.prepare(`SELECT * FROM incident WHERE ${where} ORDER BY started_at DESC LIMIT 500`).bind(...args).all()) as D1Result);
358 if (!list.length) return { list, comps: [], timeline: [], followups: [], pms: [] };
359 const ids = list.map((i) => i.id);
360 const [comps, timeline, followups, pms] = await db.batch([
361 db.prepare(`SELECT * FROM incident_component WHERE incident_id IN (${inList(ids)})`).bind(...ids),
362 db.prepare(`SELECT * FROM incident_timeline WHERE incident_id IN (${inList(ids)}) ORDER BY at, rowid`).bind(...ids),
363 db.prepare(`SELECT * FROM incident_followup WHERE incident_id IN (${inList(ids)}) ORDER BY created_at, rowid`).bind(...ids),
364 db.prepare(`SELECT * FROM postmortem WHERE incident_id IN (${inList(ids)})`).bind(...ids),
365 ]);
366 return { list, comps: rows(comps), timeline: rows(timeline), followups: rows(followups), pms: rows(pms) };
367}
368
369/** Sudo's board: open and draft incidents, the last 180 days, maintenance, and subscribers. */
370export async function board(db: D1Database, now: Date, origin: string) {
371 const since = iso(now.getTime() - 180 * DAY_MS);
372 const [data, maintenance, count] = await Promise.all([
373 adminIncidents(db, `resolved_at IS NULL OR resolved_at >= ?1`, [since]),
374 maintenanceWhere(db, `state IN ('scheduled', 'in_progress') OR ends_at >= ?1`, [iso(now.getTime() - HISTORY_DAYS * DAY_MS)], origin),
375 db.prepare(`SELECT COUNT(*) AS n FROM subscriber WHERE confirmed_at IS NOT NULL`).first<{ n: number }>(),
376 ]);
377 return {
378 incidents: data.list.map((row) => toAdmin(row, data.comps, data.timeline, data.followups, data.pms.find((p) => p.incident_id === row.id) ?? null)),
379 maintenance,
380 subscribers: count?.n ?? 0,
381 };
382}
383
384export async function openCount(db: D1Database): Promise<number> {
385 const row = await db.prepare(`SELECT COUNT(*) AS n FROM incident WHERE resolved_at IS NULL AND visibility IN ('draft', 'public')`).first<{ n: number }>();
386 return row?.n ?? 0;
387}
388
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders389export async function incidentDetail(
390 db: D1Database,
391 id: string,
392 origin: string,
393 names: Map<string, string>,
394 { limits = new Map<string, number>(), now = new Date() }: { limits?: Map<string, number>; now?: Date } = {},
395): Promise<AdminIncidentDetail | null> {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas396 const data = await adminIncidents(db, `id = ?1`, [id]);
397 const row = data.list[0];
398 if (!row) return null;
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders399 // Every check of its parts around it, for the latency chart.
400 const keys = [...new Set(data.comps.filter((c) => c.incident_id === row.id).map((c) => c.component))];
401 const span = historySpan(row.started_at, row.resolved_at, now);
402 const samples = await checkHistory(db, keys, span.from, span.to);
403 const checks: CheckHistory[] = keys.map((key) => ({
404 key,
405 slow_ms: limits.get(key) ?? null,
406 from: span.from.toISOString(),
407 to: span.to.toISOString(),
408 samples: samples.filter((s) => s.component === key),
409 }));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas410 const pmRow = data.pms[0] ?? null;
411 const incident = toAdmin(row, data.comps, data.timeline, data.followups, pmRow);
412 const timeline = data.timeline.map(toTimeline);
413 const followups: FollowUp[] = data.followups.map(({ incident_id: _, ...f }) => ({
414 id: f.id,
415 title: f.title,
416 owner: f.owner,
417 done_at: f.done_at,
418 created_at: f.created_at,
419 created_by: f.created_by,
420 }));
421 const postmortem: Postmortem | null = pmRow
422 ? {
423 summary: pmRow.summary,
424 impact: pmRow.impact,
425 timeline: pmRow.timeline,
426 root_cause: pmRow.root_cause,
427 went_well: pmRow.went_well,
428 went_badly: pmRow.went_badly,
429 action_items: pmRow.action_items,
430 updated_at: pmRow.updated_at,
431 updated_by: pmRow.updated_by,
432 published_at: pmRow.published_at,
433 }
434 : null;
435 return {
436 ...incident,
437 timeline,
438 followups,
439 postmortem,
440 postmortem_draft: postmortemDraft(incident, timeline, followups, names),
441 url: incidentUrl(origin, id),
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders442 checks,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas443 };
444}
445
446export function facts(i: AdminIncident): IncidentFacts {
447 return {
448 status: i.status,
449 severity: i.severity,
450 visibility: i.visibility,
451 components: i.components,
452 started_at: i.started_at,
453 acknowledged_at: i.acknowledged_at,
454 mitigated_at: i.mitigated_at,
455 resolved_at: i.resolved_at,
456 commander: i.commander,
457 communications: i.communications,
458 };
459}
460
461export async function maintenanceById(db: D1Database, id: string, origin: string): Promise<AdminMaintenance | null> {
462 return (await maintenanceWhere(db, `id = ?1`, [id], origin))[0] ?? null;
463}
464
465// --- Writes ------------------------------------------------------------------------
466
467export function auditStatement(db: D1Database, at: string, by: string, action: string, target: string, detail: string): D1PreparedStatement {
468 return db
469 .prepare(`INSERT INTO audit (id, at, by, action, target, detail) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
470 .bind(crypto.randomUUID(), at, by, action, target, detail.slice(0, 500));
471}
472
473/** Timeline lines, written a millisecond apart so their order survives. */
474function timelineStatements(db: D1Database, incident: string, entries: (Entry & { notified?: number | null })[], at: Date, by: string): D1PreparedStatement[] {
475 return entries.map((e, i) =>
476 db
477 .prepare(`INSERT INTO incident_timeline (id, incident_id, at, by, kind, public, status, text, notified) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`)
478 .bind(shortId(), incident, iso(at.getTime() + i), by, e.kind, e.public ? 1 : 0, e.status, e.text, e.notified ?? null),
479 );
480}
481
482function componentStatements(db: D1Database, incident: string, list: ImpactInput[]): D1PreparedStatement[] {
483 return list.map((c) =>
484 db
485 .prepare(`INSERT INTO incident_component (incident_id, component, impact) VALUES (?1, ?2, ?3) ON CONFLICT (incident_id, component) DO UPDATE SET impact = ?3`)
486 .bind(incident, c.key, c.impact),
487 );
488}
489
490export type NewIncidentRow = {
491 title: string;
492 severity: IncidentSeverity;
493 status: IncidentStatus;
494 visibility: IncidentVisibility;
495 source: "declared" | "detected";
496 components: ImpactInput[];
497 started_at: string;
498 acknowledged_at: string | null;
499 commander: string | null;
500 communications: string | null;
501 by: string;
502};
503
504/** A new incident with its first lines. Returns its id. */
505export async function createIncident(
506 db: D1Database,
507 input: NewIncidentRow,
508 entries: (Entry & { notified?: number | null })[],
509 now: Date,
510 audit: { action: string; detail: string } | null,
511 id = shortId(),
512): Promise<string> {
513 const at = now.toISOString();
514 await db.batch([
515 db
516 .prepare(
517 `INSERT INTO incident (id, title, severity, status, visibility, source, started_at, declared_at, acknowledged_at, published_at, commander, communications, created_by)
518 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)`,
519 )
520 .bind(
521 id,
522 input.title,
523 input.severity,
524 input.status,
525 input.visibility,
526 input.source,
527 input.started_at,
528 at,
529 input.acknowledged_at,
530 input.visibility === "public" ? at : null,
531 input.commander,
532 input.communications,
533 input.by,
534 ),
535 ...componentStatements(db, id, input.components),
536 ...timelineStatements(db, id, entries, now, input.by),
537 ...(audit ? [auditStatement(db, at, input.by, audit.action, id, audit.detail)] : []),
538 ]);
539 return id;
540}
541
542/** Saves an incident's changed facts and the lines that say so. */
543export async function saveIncident(
544 db: D1Database,
545 id: string,
546 next: IncidentFacts & { title?: string; published_at?: string | null },
547 entries: (Entry & { notified?: number | null })[],
548 now: Date,
549 by: string,
550 audit: { action: string; detail: string },
551): Promise<void> {
552 await db.batch([
553 db
554 .prepare(
555 `UPDATE incident SET status = ?2, severity = ?3, visibility = ?4, acknowledged_at = ?5, mitigated_at = ?6, resolved_at = ?7,
556 commander = ?8, communications = ?9, title = COALESCE(?10, title), published_at = COALESCE(published_at, ?11) WHERE id = ?1`,
557 )
558 .bind(
559 id,
560 next.status,
561 next.severity,
562 next.visibility,
563 next.acknowledged_at,
564 next.mitigated_at,
565 next.resolved_at,
566 next.commander,
567 next.communications,
568 next.title ?? null,
569 next.published_at ?? null,
570 ),
571 ...componentStatements(db, id, next.components),
572 ...timelineStatements(db, id, entries, now, by),
573 auditStatement(db, now.toISOString(), by, audit.action, id, audit.detail),
574 ]);
575}
576
577/** Lines the checks write on open incidents: failing again, or answering again. */
578export async function addSystemLines(db: D1Database, lines: { incident: string; kind: TimelineKind; text: string }[], now: Date): Promise<void> {
579 if (!lines.length) return;
580 await db.batch(lines.flatMap((l) => timelineStatements(db, l.incident, [{ kind: l.kind, public: false, status: null, text: l.text }], now, "status")));
581}
582
583export async function addFollowUp(db: D1Database, incident: string, input: { title: string; owner: string | null; by: string }, now: Date): Promise<FollowUp> {
584 const f: FollowUp = { id: shortId(), title: input.title, owner: input.owner, done_at: null, created_at: now.toISOString(), created_by: input.by };
585 await db.batch([
586 db
587 .prepare(`INSERT INTO incident_followup (id, incident_id, title, owner, created_at, created_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
588 .bind(f.id, incident, f.title, f.owner, f.created_at, f.created_by),
589 ...timelineStatements(db, incident, [{ kind: "followup", public: false, status: null, text: `Follow-up: ${f.title}${f.owner ? ` (${f.owner})` : ""}.` }], now, input.by),
590 auditStatement(db, f.created_at, input.by, "incident_followup", incident, `Added follow-up: ${f.title}`),
591 ]);
592 return f;
593}
594
595export async function setFollowUp(db: D1Database, incident: string, id: string, done: boolean, by: string, now: Date): Promise<FollowUp | null> {
596 const row = (await db.prepare(`SELECT * FROM incident_followup WHERE id = ?1 AND incident_id = ?2`).bind(id, incident).first()) as FollowUpRow | null;
597 if (!row) return null;
598 const at = now.toISOString();
599 await db.batch([
600 db.prepare(`UPDATE incident_followup SET done_at = ?2, done_by = ?3 WHERE id = ?1`).bind(id, done ? at : null, done ? by : null),
601 ...timelineStatements(db, incident, [{ kind: "followup", public: false, status: null, text: `${done ? "Done" : "Reopened"}: ${row.title}.` }], now, by),
602 auditStatement(db, at, by, "incident_followup", incident, `${done ? "Done" : "Reopened"}: ${row.title}`),
603 ]);
604 return { id, title: row.title, owner: row.owner, done_at: done ? at : null, created_at: row.created_at, created_by: row.created_by };
605}
606
607export async function savePostmortem(db: D1Database, incident: string, fields: PostmortemFields, by: string, now: Date): Promise<void> {
608 const at = now.toISOString();
609 await db.batch([
610 db
611 .prepare(
612 `INSERT INTO postmortem (incident_id, summary, impact, timeline, root_cause, went_well, went_badly, action_items, updated_at, updated_by)
613 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
614 ON CONFLICT (incident_id) DO UPDATE SET summary = ?2, impact = ?3, timeline = ?4, root_cause = ?5, went_well = ?6, went_badly = ?7,
615 action_items = ?8, updated_at = ?9, updated_by = ?10`,
616 )
617 .bind(incident, fields.summary, fields.impact, fields.timeline, fields.root_cause, fields.went_well, fields.went_badly, fields.action_items, at, by),
618 auditStatement(db, at, by, "postmortem_saved", incident, "Saved the postmortem draft"),
619 ]);
620}
621
622export async function publishPostmortem(db: D1Database, incident: string, publish: boolean, by: string, now: Date): Promise<void> {
623 const at = now.toISOString();
624 await db.batch([
625 db.prepare(`UPDATE postmortem SET published_at = ?2, published_by = ?3 WHERE incident_id = ?1`).bind(incident, publish ? at : null, publish ? by : null),
626 ...timelineStatements(db, incident, [{ kind: "postmortem", public: false, status: null, text: publish ? "Postmortem published." : "Postmortem taken down." }], now, by),
627 auditStatement(db, at, by, publish ? "postmortem_published" : "postmortem_unpublished", incident, publish ? "Published the postmortem" : "Took the postmortem down"),
628 ]);
629}
630
631// --- Maintenance ---------------------------------------------------------------------
632
633export async function scheduleMaintenance(
634 db: D1Database,
635 input: { title: string; message: string; components: string[]; starts_at: string; ends_at: string; notify: boolean; by: string },
636 notified: number | null,
637 now: Date,
638 id = shortId(),
639): Promise<string> {
640 const at = now.toISOString();
641 await db.batch([
642 db
643 .prepare(
644 `INSERT INTO maintenance (id, title, message, components, starts_at, ends_at, state, notify, created_at, created_by)
645 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'scheduled', ?7, ?8, ?9)`,
646 )
647 .bind(id, input.title, input.message, JSON.stringify(input.components), input.starts_at, input.ends_at, input.notify ? 1 : 0, at, input.by),
648 db
649 .prepare(`INSERT INTO maintenance_update (id, maintenance_id, at, by, text, notified) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
650 .bind(shortId(), id, at, input.by, input.message, notified),
651 auditStatement(db, at, input.by, "maintenance_scheduled", id, `${input.title} (${input.starts_at} to ${input.ends_at})`),
652 ]);
653 return id;
654}
655
656/** Moves maintenance along and posts its update. Null `state` keeps it where it is. */
657export async function maintenanceUpdate(
658 db: D1Database,
659 id: string,
660 state: MaintenanceState | null,
661 text: string,
662 by: string,
663 notified: number | null,
664 now: Date,
665 audit: { action: string; detail: string } | null,
666): Promise<void> {
667 const at = now.toISOString();
668 await db.batch([
669 ...(state ? [db.prepare(`UPDATE maintenance SET state = ?2 WHERE id = ?1`).bind(id, state)] : []),
670 db
671 .prepare(`INSERT INTO maintenance_update (id, maintenance_id, at, by, text, notified) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`)
672 .bind(shortId(), id, at, by, text, notified),
673 ...(audit ? [auditStatement(db, at, by, audit.action, id, audit.detail)] : []),
674 ]);
675}
676
677/** Maintenance whose window has started or ended but whose state has not caught up. */
678export async function dueMaintenance(db: D1Database, now: Date, origin: string): Promise<(AdminMaintenance & { notify: boolean })[]> {
679 const at = now.toISOString();
680 const list = rows<MaintenanceRow>(
681 (await db
682 .prepare(`SELECT * FROM maintenance WHERE (state = 'scheduled' AND starts_at <= ?1) OR (state = 'in_progress' AND ends_at <= ?1)`)
683 .bind(at)
684 .all()) as D1Result,
685 );
686 return list.map((row) => ({ ...toMaintenance(row, [], origin), notify: row.notify === 1 }));
687}
688
689// --- Subscribers -----------------------------------------------------------------------
690
691type SubscriberRow = {
692 id: string;
693 email: string;
694 components: string | null;
695 pending_components: string | null;
696 confirm_hash: string | null;
697 confirm_expires_at: string | null;
698 confirm_sent_at: string | null;
699 confirmed_at: string | null;
700};
701
702/**
703 * Asks for a subscription (or a change of parts): a new confirmation,
704 * unless one went out to this address in the last `resendAfterMs`. The
705 * caller answers the same either way, so the form never tells whether an
706 * address is subscribed.
707 */
708export async function requestSubscription(
709 db: D1Database,
710 email: string,
711 parts: string[] | null,
712 hash: string,
713 now: Date,
714 ttlMs: number,
715 resendAfterMs: number,
716): Promise<{ send: boolean }> {
717 const at = now.toISOString();
718 const existing = (await db.prepare(`SELECT * FROM subscriber WHERE email = ?1`).bind(email).first()) as SubscriberRow | null;
719 if (existing?.confirm_sent_at && now.getTime() - Date.parse(existing.confirm_sent_at) < resendAfterMs) return { send: false };
720 const pending = parts ? JSON.stringify(parts) : null;
721 if (existing) {
722 await db
723 .prepare(`UPDATE subscriber SET pending_components = ?2, confirm_hash = ?3, confirm_expires_at = ?4, confirm_sent_at = ?5 WHERE id = ?1`)
724 .bind(existing.id, pending, hash, iso(now.getTime() + ttlMs), at)
725 .run();
726 } else {
727 await db
728 .prepare(
729 `INSERT INTO subscriber (id, email, components, pending_components, confirm_hash, confirm_expires_at, confirm_sent_at, created_at)
730 VALUES (?1, ?2, NULL, ?3, ?4, ?5, ?6, ?6)`,
731 )
732 .bind(crypto.randomUUID(), email, pending, hash, iso(now.getTime() + ttlMs), at)
733 .run();
734 }
735 return { send: true };
736}
737
738/** Confirms the subscription a token's hash belongs to; null when none or expired. */
739export async function confirmSubscription(db: D1Database, hash: string, now: Date): Promise<{ email: string; parts: string[] | null } | null> {
740 const row = (await db.prepare(`SELECT * FROM subscriber WHERE confirm_hash = ?1`).bind(hash).first()) as SubscriberRow | null;
741 if (!row || !row.confirm_expires_at || Date.parse(row.confirm_expires_at) < now.getTime()) return null;
742 await db
743 .prepare(
744 `UPDATE subscriber SET components = pending_components, pending_components = NULL, confirm_hash = NULL, confirm_expires_at = NULL,
745 confirmed_at = COALESCE(confirmed_at, ?2) WHERE id = ?1`,
746 )
747 .bind(row.id, now.toISOString())
748 .run();
749 return { email: row.email, parts: parseParts(row.pending_components) };
750}
751
752export async function unsubscribe(db: D1Database, id: string): Promise<boolean> {
753 const result = await db.prepare(`DELETE FROM subscriber WHERE id = ?1`).bind(id).run();
754 return (result.meta?.changes ?? 0) > 0;
755}
756
757/** Confirmed subscribers who want news about these parts. */
758export async function recipients(db: D1Database, about: string[]): Promise<{ id: string; email: string }[]> {
759 const all = rows<SubscriberRow>((await db.prepare(`SELECT id, email, components FROM subscriber WHERE confirmed_at IS NOT NULL`).all()) as D1Result);
760 return all.filter((s) => wants(parseParts(s.components), about)).map((s) => ({ id: s.id, email: s.email }));
761}
762
763// --- Detection ---------------------------------------------------------------------------
764
765export async function loadStreaks(db: D1Database): Promise<Map<string, Streak>> {
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders766 const list = rows<{ component: string; state: "degraded" | "down"; count: number; checks: number | null; recent: string | null; since: string; alerted: number }>(
767 (await db.prepare(`SELECT component, state, count, checks, recent, since, alerted FROM streak`).all()) as D1Result,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas768 );
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders769 // Runs kept before migration 0003 have no `checks` or `recent`: upgradeStreak fills them in.
770 return new Map(list.map((s) => [s.component, upgradeStreak({ ...s, alerted: s.alerted === 1 })]));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas771}
772
Fast pages, required checks on the branch, self-hosted runners, honest incidents773/**
774 * Keeps exactly these runs: every other part's run is over. With none
775 * left, every row goes (`NOT IN (NULL)` matches nothing in SQL, which once
776 * kept a finished run alive and let its old start leak into the next one).
777 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas778export async function saveStreaks(db: D1Database, streaks: Streak[]): Promise<void> {
779 const keep = streaks.map((s) => s.component);
780 await db.batch([
Fast pages, required checks on the branch, self-hosted runners, honest incidents781 keep.length ? db.prepare(`DELETE FROM streak WHERE component NOT IN (${inList(keep)})`).bind(...keep) : db.prepare(`DELETE FROM streak`),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas782 ...streaks.map((s) =>
783 db
784 .prepare(
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders785 `INSERT INTO streak (component, state, count, since, alerted, checks, recent) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
786 ON CONFLICT (component) DO UPDATE SET state = ?2, count = ?3, since = ?4, alerted = ?5, checks = ?6, recent = ?7`,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas787 )
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders788 .bind(s.component, s.state, s.count, s.since, s.alerted ? 1 : 0, s.checks, s.recent),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas789 ),
790 ]);
791}
792
793/** Open incidents, drafts included, with the parts they affect: what detection checks against. */
794export async function openRefs(db: D1Database): Promise<{ id: string; components: string[] }[]> {
795 const list = rows<{ id: string; component: string | null }>(
796 (await db
797 .prepare(
798 `SELECT i.id, c.component FROM incident i LEFT JOIN incident_component c ON c.incident_id = i.id AND c.impact != 'operational'
799 WHERE i.resolved_at IS NULL AND i.visibility IN ('draft', 'public')`,
800 )
801 .all()) as D1Result,
802 );
803 const map = new Map<string, string[]>();
804 for (const r of list) {
805 const parts = map.get(r.id) ?? [];
806 if (r.component) parts.push(r.component);
807 map.set(r.id, parts);
808 }
809 return [...map].map(([id, components]) => ({ id, components }));
810}
811
Fast pages, required checks on the branch, self-hosted runners, honest incidents812/** The last deploy the deploy tool reported, kept in `meta`. */
813export async function loadDeploy(db: D1Database): Promise<DeployWindow | null> {
814 const row = (await db.prepare(`SELECT value FROM meta WHERE key = 'deploy'`).first()) as { value?: string } | null;
815 if (!row?.value) return null;
816 try {
817 const w = JSON.parse(row.value) as Partial<DeployWindow>;
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders818 if (typeof w.started_at !== "string") return null;
819 const finished_at = w.finished_at ?? null;
820 return {
821 id: w.id ?? null,
822 started_at: w.started_at,
823 finished_at,
824 // Kept before deploys were counted: one, unless it finished.
825 running: typeof w.running === "number" ? w.running : finished_at ? 0 : 1,
826 last_started_at: typeof w.last_started_at === "string" ? w.last_started_at : w.started_at,
827 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents828 } catch {
829 return null;
830 }
831}
832
833export async function saveDeploy(db: D1Database, window: DeployWindow): Promise<void> {
834 await db
835 .prepare(`INSERT INTO meta (key, value) VALUES ('deploy', ?1) ON CONFLICT (key) DO UPDATE SET value = ?1`)
836 .bind(JSON.stringify(window))
837 .run();
838}
839
840const HEALTHY = "draft_healthy:";
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders841const REMINDED = "draft_reminded:";
Fast pages, required checks on the branch, self-hosted runners, honest incidents842
843/**
844 * Detected drafts no one has picked up (unacknowledged, never published),
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders845 * with their parts, since when those have been healthy, and when the
846 * alert last went out again for them.
Fast pages, required checks on the branch, self-hosted runners, honest incidents847 */
848export async function watchedDrafts(db: D1Database): Promise<WatchedDraft[]> {
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders849 const list = rows<{
850 id: string;
851 title: string;
852 started_at: string;
853 declared_at: string;
854 healthy_since: string | null;
855 reminded_at: string | null;
856 component: string | null;
857 }>(
Fast pages, required checks on the branch, self-hosted runners, honest incidents858 (await db
859 .prepare(
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders860 `SELECT i.id, i.title, i.started_at, i.declared_at, m.value AS healthy_since, r.value AS reminded_at, c.component FROM incident i
Fast pages, required checks on the branch, self-hosted runners, honest incidents861 LEFT JOIN incident_component c ON c.incident_id = i.id AND c.impact != 'operational'
862 LEFT JOIN meta m ON m.key = '${HEALTHY}' || i.id
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders863 LEFT JOIN meta r ON r.key = '${REMINDED}' || i.id
Fast pages, required checks on the branch, self-hosted runners, honest incidents864 WHERE i.source = 'detected' AND i.visibility = 'draft' AND i.acknowledged_at IS NULL AND i.resolved_at IS NULL AND i.published_at IS NULL`,
865 )
866 .all()) as D1Result,
867 );
868 const map = new Map<string, WatchedDraft>();
869 for (const r of list) {
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders870 const d = map.get(r.id) ?? {
871 id: r.id,
872 title: r.title,
873 started_at: r.started_at,
874 declared_at: r.declared_at,
875 healthy_since: r.healthy_since ?? null,
876 reminded_at: r.reminded_at ?? null,
877 components: [],
878 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents879 if (r.component) d.components.push(r.component);
880 map.set(r.id, d);
881 }
882 return [...map.values()];
883}
884
885/** Keeps each watched draft's healthy-since, and forgets drafts no longer watched. */
886export async function saveHealthy(db: D1Database, healthy: { id: string; since: string | null }[]): Promise<void> {
887 const keys = healthy.filter((h) => h.since).map((h) => `${HEALTHY}${h.id}`);
888 await db.batch([
889 keys.length
890 ? db.prepare(`DELETE FROM meta WHERE key LIKE '${HEALTHY}%' AND key NOT IN (${inList(keys)})`).bind(...keys)
891 : db.prepare(`DELETE FROM meta WHERE key LIKE '${HEALTHY}%'`),
892 ...healthy
893 .filter((h) => h.since)
894 .map((h) =>
895 db
896 .prepare(`INSERT INTO meta (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value WHERE meta.value != excluded.value`)
897 .bind(`${HEALTHY}${h.id}`, h.since),
898 ),
899 ]);
900}
901
902/**
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders903 * Notes that the alert went out again for these drafts (`staleDrafts`),
904 * with a line on each one's timeline, and forgets the drafts no longer
905 * watched (`watching`: every watched draft's id).
906 */
907export async function saveReminders(db: D1Database, watching: string[], reminded: { id: string; text: string }[], now: Date): Promise<void> {
908 const keys = watching.map((id) => `${REMINDED}${id}`);
909 await db.batch([
910 keys.length
911 ? db.prepare(`DELETE FROM meta WHERE key LIKE '${REMINDED}%' AND key NOT IN (${inList(keys)})`).bind(...keys)
912 : db.prepare(`DELETE FROM meta WHERE key LIKE '${REMINDED}%'`),
913 ...reminded.flatMap((r) => [
914 db
915 .prepare(`INSERT INTO meta (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value`)
916 .bind(`${REMINDED}${r.id}`, now.toISOString()),
917 ...timelineStatements(db, r.id, [{ kind: "note", public: false, status: null, text: r.text }], now, "status"),
918 ]),
919 ]);
920}
921
922/**
Fast pages, required checks on the branch, self-hosted runners, honest incidents923 * Dismisses a detected draft that recovered, as `status`, resolved at the
924 * moment it recovered. Only while it is still an untouched draft: returns
925 * false (and changes nothing) when staff got to it first.
926 */
927export async function autoDismiss(db: D1Database, id: string, recoveredAt: string, text: string, now: Date): Promise<boolean> {
928 const done = await db
929 .prepare(
930 `UPDATE incident SET visibility = 'dismissed', status = 'resolved', resolved_at = ?2
931 WHERE id = ?1 AND visibility = 'draft' AND source = 'detected' AND acknowledged_at IS NULL AND resolved_at IS NULL`,
932 )
933 .bind(id, recoveredAt)
934 .run();
935 if ((done.meta?.changes ?? 0) === 0) return false;
936 await db.batch([
937 ...timelineStatements(db, id, [{ kind: "dismissed", public: false, status: null, text }], now, "status"),
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders938 db.prepare(`DELETE FROM meta WHERE key IN (?1, ?2)`).bind(`${HEALTHY}${id}`, `${REMINDED}${id}`),
Fast pages, required checks on the branch, self-hosted runners, honest incidents939 auditStatement(db, now.toISOString(), "status", "incident_dismissed", id, `Dismissed automatically: ${text}`),
940 ]);
941 return true;
942}
943
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas944// --- Audit ---------------------------------------------------------------------------------
945
946export async function auditLog(db: D1Database, before: string | null, limit = 100): Promise<StatusAuditEntry[]> {
947 const statement = before
948 ? db.prepare(`SELECT * FROM audit WHERE at < ?1 ORDER BY at DESC LIMIT ?2`).bind(before, limit)
949 : db.prepare(`SELECT * FROM audit ORDER BY at DESC LIMIT ?1`).bind(limit);
950 return rows<StatusAuditEntry>((await statement.all()) as D1Result);
951}

This file's history is long; its oldest lines are credited to the oldest commit read.