Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
1568e48
feat(alerts): acknowledge alert incidents (core, API, MCP)
Makisuo Nov 6, 2026
5a66d2b
fix(alerts): clear an ack on escalation in the claimed write, decided…
Makisuo Nov 6, 2026
090cbf6
Merge remote-tracking branch 'origin/main' into feat/alert-ack-core
Makisuo Nov 6, 2026
836f27a
chore(ios): regenerate the OpenAPI spec for the incident acknowledgem…
Makisuo Nov 6, 2026
7ec39de
fix(alerts): keep the acknowledged filter on the incident list's next…
Makisuo Nov 6, 2026
9eaf37e
chore(domain): regenerate anticipated errors for AlertIncidentResolve…
Makisuo Nov 6, 2026
6577df5
fix(alerts): leave the internal incident id out of the resolved-incid…
Makisuo Nov 6, 2026
5abdc40
fix(alerts): attribute MCP acks like the audit log, stamp ack_cleared…
Makisuo Nov 6, 2026
d4b37ee
chore(alerts): yield before the call
Makisuo Nov 6, 2026
6a09658
Merge remote-tracking branch 'origin/main' into feat/alert-ack-core
Makisuo Nov 6, 2026
f93853b
Merge remote-tracking branch 'origin/main' into feat/alert-ack-core
Makisuo Nov 6, 2026
a929a95
feat(alerts): escalation chains that page step by step until someone …
Makisuo Nov 6, 2026
0a0e10a
test(mcp): acknowledge by inc_ id and assert the rendered text
Makisuo Nov 6, 2026
eac5819
test(alerts): escalation chains walk, repeat, stop on ack and resolve…
Makisuo Nov 6, 2026
17b8598
feat(alerts): escalation chains in the v2 API and MCP, and rule tiers…
Makisuo Nov 6, 2026
dcc44b1
Merge remote-tracking branch 'origin/feat/alert-ack-core' into feat/a…
Makisuo Nov 6, 2026
1598b16
feat(alerts): pure on-call schedule resolver
Makisuo Nov 6, 2026
3924399
fix(alerts): use Arr.findFirst for the delivery log's connector lookup
Makisuo Nov 6, 2026
9416058
feat(alerts): v2 alert incidents carry their latest escalation run
Makisuo Nov 6, 2026
16ae390
fix(alerts): escalation runs survive flapping, stop on de-escalation,…
Makisuo Nov 6, 2026
327bcce
feat(alerts): on-call schedules with layers, rotations and overrides
Makisuo Nov 6, 2026
5a8f016
feat(alerts): on-call schedules in the v2 API and MCP
Makisuo Nov 6, 2026
a697395
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
2e030b1
fix(web): a tier routed only by its escalation chain saves from the r…
Makisuo Nov 6, 2026
f8a2a0c
feat(alerts): escalation chains in the Routing tab, with a dialog to …
Makisuo Nov 6, 2026
909b57c
feat(alerts): pick an escalation chain on each rule tier
Makisuo Nov 6, 2026
e57fbf5
feat(alerts): the incident rail shows its escalation: escalating, pau…
Makisuo Nov 6, 2026
e98730c
feat(lab): /lab/escalation for chains, the chain dialog, the tier pic…
Makisuo Nov 6, 2026
21f887c
test(web): a stored tier's escalation chain survives ruleToFormState …
Makisuo Nov 6, 2026
3f7e164
Merge branch 'feat/alert-escalation-chains' into feat/alert-escalatio…
Makisuo Nov 6, 2026
71737b0
test(web): drop the tier routing test the chains branch already covers
Makisuo Nov 6, 2026
c255bab
fix(web): the last step's wait label reads the parsed repeat count an…
Makisuo Nov 6, 2026
bdf9704
feat(alerts): page the on-call by email and chat direct message
Makisuo Nov 6, 2026
bd44ecb
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
37b2bca
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
8168fa5
fix(web): the chain delete conflict reads well when the cached chain …
Makisuo Nov 6, 2026
46dd4da
fix(lint): effect/Array.findFirst for the delivery log's connector lo…
Makisuo Nov 6, 2026
db88c86
Merge remote-tracking branch 'origin/feat/alert-ack-core' into feat/a…
Makisuo Nov 6, 2026
2d08f9b
fix(web): editing an escalation chain keeps each step's member and sc…
Makisuo Nov 6, 2026
375d9dc
fix(web): the incident rail dates a next page due on another day and …
Makisuo Nov 6, 2026
9439738
fix(web): the incident rail adds no escalation section while the read…
Makisuo Nov 6, 2026
f84d141
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 6, 2026
b8defc3
chore(mcp): regenerate tool descriptors after merging main
Makisuo Nov 6, 2026
9ffa299
fix(api): a pre-1970 on-call timestamp is a 400, not a thrown constru…
Makisuo Nov 6, 2026
eef6186
fix(chat): connectors declare direct messages, and a rate limit retri…
Makisuo Nov 6, 2026
0b0094d
fix(alerts): member pages try every linked workspace, skip departed m…
Makisuo Nov 6, 2026
1af08d9
fix(alerts): schedules without a member directory, names without emai…
Makisuo Nov 6, 2026
d301b1b
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
f6c33ca
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
0a6a76b
fix(alerts): an escalation step re-reads its incident before sending;…
Makisuo Nov 6, 2026
2893824
refactor(alerts): pages read in their own service, so acknowledging n…
Makisuo Nov 6, 2026
4221fa8
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
ab96cb0
fix(mcp): delete_escalation_chain's description says a chain-only tie…
Makisuo Nov 6, 2026
81bd621
fix(alerts): escalation spans record the org as orgId; the detach hin…
Makisuo Nov 6, 2026
9bff7e5
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
5483d7c
fix(alerts): the escalation chain dialog locks its form while a save …
Makisuo Nov 6, 2026
849d6a5
fix(alerts): the escalation dialog's wait and repeat ranges come from…
Makisuo Nov 6, 2026
18b5f85
fix(lab): the escalation lab keeps an allowlist of local commands
Makisuo Nov 6, 2026
3c3f45f
fix(alerts): stale escalation steps are dropped from chat digests too…
Makisuo Nov 6, 2026
3c8d438
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
b8595a9
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
fb84b4c
fix(alerts): a destination a running escalation still pages cannot be…
Makisuo Nov 6, 2026
2cf0bc9
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
493a25e
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
fc45bd8
fix(web): each tier's escalation chain picker has its own accessible …
Makisuo Nov 6, 2026
7085053
fix(alerts): the pages read cap drops the oldest pages, never the newest
Makisuo Nov 6, 2026
a1dc739
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
2e585cf
fix(web): each step's wait input is named by its step; an unacknowled…
Makisuo Nov 6, 2026
45143a8
fix(web): a failed or skipped page reads as such in the incident time…
Makisuo Nov 6, 2026
2f60269
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
29a7171
fix(alerts): on-call bounds are the real handoffs, not the lookaround…
Makisuo Nov 6, 2026
b5b7237
fix(api): on-call 400s name the wire field and what is wrong with it
Makisuo Nov 6, 2026
e1ebead
fix(alerts): incident timelines say why a page failed and read a queu…
Makisuo Nov 6, 2026
aa10819
fix(alerts): a page stopped by an ack after failed attempts says it s…
Makisuo Nov 6, 2026
2ce204b
test(mcp): import the shift document type the on-call tool stub uses
Makisuo Nov 6, 2026
02876c9
fix(alerts): an email page fails at once where the deployment cannot …
Makisuo Nov 6, 2026
671937b
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
7f1a1e6
fix(web): a refused chain delete counts rules it cannot name instead …
Makisuo Nov 6, 2026
8321668
test(web): a chain's rule count reads before the rules list loads
Makisuo Nov 6, 2026
de6959d
fix(api): v2 escalation chain step errors say what is wrong in wire t…
Makisuo Nov 6, 2026
254950e
fix(alerts): destination in-use conflict names escalation chains too
Makisuo Nov 6, 2026
74158df
fix(mcp): escalation chain tools report bad fields on their parameter…
Makisuo Nov 6, 2026
e721c66
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
72e7b2e
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
2c1ffc6
fix(mcp): update_escalation_chain clears notes with description "none"
Makisuo Nov 6, 2026
7b80706
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
d09a2db
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
66d2375
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 6, 2026
1352903
feat(alerts): a silence holds an escalation's due step until it ends
Makisuo Nov 6, 2026
2880947
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
e801830
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
2cd8b77
fix(alerts): a held step skips a destination the silence's renotify c…
Makisuo Nov 6, 2026
7af3d92
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
89a3704
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
a99c2ad
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 6, 2026
9bf3fb1
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
64c91c6
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
26e332d
fix(web): a failed chain read keeps the tier's chain instead of calli…
Makisuo Nov 6, 2026
5bf5828
fix(web): the rule editor rereads chains on returning to the tab; cha…
Makisuo Nov 6, 2026
c51738f
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 6, 2026
2a3582f
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
eb4330e
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
62aeecf
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 6, 2026
a5a2302
Merge branch 'feat/alert-escalation-chains' into feat/alert-oncall-sc…
Makisuo Nov 6, 2026
7f4c928
Merge branch 'feat/alert-oncall-schedules' into feat/alert-escalation…
Makisuo Nov 6, 2026
77eab5a
fix(web): a superseded run reads as a severity change; a failed chain…
Makisuo Nov 6, 2026
7354b65
Merge remote-tracking branch 'origin/main' into feat/alert-escalation…
Makisuo Nov 7, 2026
b466b1c
fix(web): an anomaly rule's unrouted tier shows no routing error
Makisuo Nov 7, 2026
4cfbd48
fix(alerts): an acked run reads paused at once; the incident rail rer…
Makisuo Nov 7, 2026
548b0f7
fix(web): a refused chain delete rereads the chains and counts its bl…
Makisuo Nov 7, 2026
fbba600
fix(web): the chain picker is described by and marked invalid with it…
Makisuo Nov 7, 2026
f92b112
fix(web): a refused chain delete's message is rebuilt from the chains…
Makisuo Nov 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions apps/api/src/routes/v2/alert-incidents.http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { HttpApiBuilder } from "effect/http-api"
import type { AlertIncidentDocument, AlertIncidentEventDocument } from "@maple/domain/http"
import type {
AlertIncidentDocument,
AlertIncidentEscalationDocument,
AlertIncidentEventDocument,
AlertIncidentId,
OrgId,
} from "@maple/domain/http"
import { CurrentTenant, isAlertNotificationEventType } from "@maple/domain/http"
import type { V2AlertIncident, V2AlertIncidentEvent } from "@maple/domain/http/v2"
import type { V2AlertIncident, V2AlertIncidentEscalation, V2AlertIncidentEvent } from "@maple/domain/http/v2"
import { MapleApiV2, paginateOffsetQuery, timestampOrNull } from "@maple/domain/http/v2"
import { Duration, Effect, Option, Array as Arr } from "effect"
import { AlertReadModelsService } from "@maple/backend/services/alerts/AlertReadModelsService"
Expand All @@ -16,7 +22,33 @@ import {
import { OrgMembersService } from "@maple/backend/services/org/OrgMembersService"
import { CurrentAuditActor } from "@maple/backend/services/auth/audit-actor"

const toV2Incident = (doc: AlertIncidentDocument): V2AlertIncident => ({
/**
* An ack pauses the run on the scheduler's next tick; until then the wire already reads it paused,
* so a caller never sees a page coming that will not.
*/
const toV2Escalation = (
doc: AlertIncidentEscalationDocument,
isAcknowledged: boolean,
): V2AlertIncidentEscalation => {
const isPausing = isAcknowledged && doc.status === "active"
return {
chain_id: doc.chainId,
chain_name: doc.chainName,
status: isPausing ? "acknowledged" : doc.status,
// The scheduler leaves an exhausted run past its last step; the wire never reads past the end.
step: Math.min(doc.stepIndex + 1, doc.stepCount),
step_count: doc.stepCount,
pass: Math.min(doc.passIndex + 1, doc.repeatCount + 1),
pass_count: doc.repeatCount + 1,
steps_sent: doc.stepsSent,
next_step_at: isPausing ? null : doc.nextStepAt,
}
}

const toV2Incident = (
doc: AlertIncidentDocument,
escalation: AlertIncidentEscalationDocument | undefined,
): V2AlertIncident => ({
id: doc.id,
object: "alert_incident",
rule_id: doc.ruleId,
Expand Down Expand Up @@ -50,6 +82,7 @@ const toV2Incident = (doc: AlertIncidentDocument): V2AlertIncident => ({
: { user_id: doc.acknowledgedByUserId, name: doc.acknowledgedByName },
acknowledged_via: doc.acknowledgedVia,
error_issue_id: doc.errorIssueId,
escalation: escalation === undefined ? null : toV2Escalation(escalation, doc.acknowledgedAt !== null),
...(doc.anomalyIncidentId === undefined ? undefined : { anomaly_incident_id: doc.anomalyIncidentId }),
...(doc.groupLabel === undefined ? undefined : { group_label: doc.groupLabel }),
...(doc.labels === undefined ? undefined : { labels: doc.labels }),
Expand Down Expand Up @@ -88,6 +121,24 @@ export const HttpV2AlertIncidentsLive = HttpApiBuilder.group(MapleApiV2, "alertI
const pagesService = yield* AlertIncidentPagesService
const members = yield* OrgMembersService

// One escalation read for a whole page of incidents, never one per incident.
const withEscalations = (orgId: OrgId, docs: ReadonlyArray<AlertIncidentDocument>) =>
readModels
.listIncidentEscalations(
orgId,
Arr.map(docs, (doc): AlertIncidentId => doc.id),
)
.pipe(
Effect.map((escalations) => {
const byIncident = new Map(Arr.map(escalations, (row) => [row.incidentId, row]))
return Arr.map(docs, (doc) => toV2Incident(doc, byIncident.get(doc.id)))
}),
)
const withEscalation = (orgId: OrgId, doc: AlertIncidentDocument) =>
Effect.map(readModels.listIncidentEscalations(orgId, [doc.id]), (rows) =>
toV2Incident(doc, rows[0]),
)

// Who is acknowledging, with a name recorded alongside so every surface can show it without a
// directory lookup. An API key is not a person: it is named by its label and has no user. A
// session is named by the member directory, best effort: a slow one records no name.
Expand Down Expand Up @@ -128,7 +179,11 @@ export const HttpV2AlertIncidentsLive = HttpApiBuilder.group(MapleApiV2, "alertI
limit,
offset,
})
.pipe(Effect.map((response) => Arr.map(response.incidents, toV2Incident))),
.pipe(
Effect.flatMap((response) =>
withEscalations(tenant.orgId, response.incidents),
),
),
)
return { object: "list" as const, ...page }
}),
Expand All @@ -137,22 +192,21 @@ export const HttpV2AlertIncidentsLive = HttpApiBuilder.group(MapleApiV2, "alertI
Effect.gen(function* () {
const tenant = yield* CurrentTenant.Context
const incident = yield* readModels.getIncident(tenant.orgId, params.id)

return toV2Incident(incident)
return yield* withEscalation(tenant.orgId, incident)
}),
)
.handle("acknowledge", ({ params }) =>
Effect.gen(function* () {
const { orgId, actor } = yield* callerActor
const result = yield* acks.acknowledge(orgId, params.id, actor)
return toV2Incident(result.incident)
return yield* withEscalation(orgId, result.incident)
}).pipe(Effect.withSpan("HttpV2AlertIncidents.acknowledge")),
)
.handle("unacknowledge", ({ params }) =>
Effect.gen(function* () {
const { orgId, actor } = yield* callerActor
const result = yield* acks.unacknowledge(orgId, params.id, actor)
return toV2Incident(result.incident)
return yield* withEscalation(orgId, result.incident)
}).pipe(Effect.withSpan("HttpV2AlertIncidents.unacknowledge")),
)
.handle("listEvents", ({ params }) =>
Expand Down
96 changes: 96 additions & 0 deletions apps/api/src/routes/v2/alerts.http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,102 @@ describe("v2 alerts over HTTP", () => {
await harness.dispose()
})

it("reads an incident's latest escalation run on retrieve and list, and null without one", async () => {
const harness = makeHarness()
const key = await harness.bootstrapKey(["alerts:write"])
await createWebhookAndRule(harness, key.secret)
await executeSql(
harness.testDb,
`insert into alert_incidents (
id, org_id, rule_id, incident_key, rule_name, group_key, signal_type, severity, status,
comparator, threshold, first_triggered_at, last_triggered_at, resolved_at, dedupe_key,
created_at, updated_at
)
select v.id, rules.org_id, rules.id, v.id, 'Checkout error rate', v.group_key, 'error_rate',
'critical', 'open', 'gt', 0.05, now(), now(), null, v.id, now(), now()
from (values
('00000000-0000-4000-8000-0000000000b1', 'checkout'),
('00000000-0000-4000-8000-0000000000b2', 'payments')
) as v(id, group_key)
cross join lateral (
select id, org_id from alert_rules where org_id = 'org_alerts_e2e' limit 1
) as rules`,
)
const steps = JSON.stringify([
{
targets: [{ type: "destination", destinationId: "00000000-0000-4000-8000-0000000000d1" }],
waitMinutes: 10,
},
{
targets: [{ type: "destination", destinationId: "00000000-0000-4000-8000-0000000000d2" }],
waitMinutes: 15,
},
{
targets: [{ type: "destination", destinationId: "00000000-0000-4000-8000-0000000000d3" }],
waitMinutes: 5,
},
])
// An older superseded run and the live one: the read shows the newer.
await executeSql(
harness.testDb,
`insert into alert_escalation_runs (
id, org_id, incident_id, chain_id, chain_name, severity, steps_json, repeat_count,
step_index, pass_index, next_step_at, status, payload_json, steps_sent, created_at, updated_at
) values
('00000000-0000-4000-8000-0000000000c1', 'org_alerts_e2e', '00000000-0000-4000-8000-0000000000b1',
'00000000-0000-4000-8000-0000000000e1', 'Old chain', 'warning', '${steps}'::jsonb, 0,
1, 0, '2026-11-06T14:00:00.000Z', 'superseded', '{}'::jsonb, 1,
'2026-11-06T13:00:00.000Z', '2026-11-06T13:00:00.000Z'),
('00000000-0000-4000-8000-0000000000c2', 'org_alerts_e2e', '00000000-0000-4000-8000-0000000000b1',
'00000000-0000-4000-8000-0000000000e2', 'Checkout on-call', 'critical', '${steps}'::jsonb, 1,
1, 0, '2026-11-06T14:25:00.000Z', 'active', '{}'::jsonb, 1,
'2026-11-06T14:10:00.000Z', '2026-11-06T14:10:00.000Z')`,
)
const list = await harness.request("GET", "/v2/alerts/incidents?status=open", key.secret)
expect(list.status).toBe(200)
const isEscalating = (incident: { escalation: unknown }) => incident.escalation !== null
const without = Arr.filter(list.body.data, (incident) => !isEscalating(incident))
const withRuns = Arr.filter(list.body.data, isEscalating)
expect(without).toHaveLength(1)
expect(without[0].escalation).toBeNull()
expect(withRuns).toHaveLength(1)
const withRun = withRuns[0]
const expected = {
chain_id: expect.stringMatching(/^esc_/),
chain_name: "Checkout on-call",
status: "active",
step: 2,
step_count: 3,
pass: 1,
pass_count: 2,
steps_sent: 1,
next_step_at: "2026-11-06T14:25:00.000Z",
}
expect(withRun.escalation).toEqual(expected)

const retrieved = await harness.request("GET", `/v2/alerts/incidents/${withRun.id}`, key.secret)
expect(retrieved.status).toBe(200)
expect(retrieved.body.escalation).toEqual(expected)

// Acked before the scheduler pauses the run: it already reads paused, with no next page.
await executeSql(
harness.testDb,
`update alert_incidents set acknowledged_at = now() where id = '00000000-0000-4000-8000-0000000000b1'`,
)
const pausing = await harness.request("GET", `/v2/alerts/incidents/${withRun.id}`, key.secret)
expect(pausing.body.escalation).toMatchObject({ status: "acknowledged", next_step_at: null })

// Paused while acknowledged: no next page time.
await executeSql(
harness.testDb,
`update alert_escalation_runs set status = 'acknowledged' where id = '00000000-0000-4000-8000-0000000000c2'`,
)
const paused = await harness.request("GET", `/v2/alerts/incidents/${withRun.id}`, key.secret)
expect(paused.body.escalation).toMatchObject({ status: "acknowledged", next_step_at: null })

await harness.dispose()
})

it("enforces the alerts scope family and rejects malformed ids", async () => {
const harness = makeHarness()
const readOnly = await harness.bootstrapKey(["alerts:read"])
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v2/v2-test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ export const AlertDestinationsServiceStubLayer = Layer.succeed(
const alertReadModelStubs = {
listIncidents: die,
getIncident: die,
listIncidentEscalations: die,
listRuleChecks: die,
summarizeRuleChecks: die,
listDeliveryEvents: die,
Expand Down
110 changes: 110 additions & 0 deletions apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@
"comparator": "gt",
"dedupe_key": "alrt_gU26thvJECdQvu54Ad9jiz:__total__",
"error_issue_id": null,
"escalation": {
"chain_id": "esc_gQtGH2s65G6a7sJmuayXL6",
"chain_name": "Checkout on-call",
"next_step_at": null,
"pass": 1,
"pass_count": 2,
"status": "acknowledged",
"step": 2,
"step_count": 3,
"steps_sent": 1
},
"first_triggered_at": "2026-07-15T09:10:00.000Z",
"group_key": null,
"held_since": null,
Expand Down Expand Up @@ -368,6 +379,10 @@
"$ref": "#/components/schemas/_maple_ErrorIssueId",
"description": "The linked error issue (`iss_…`) when the incident was correlated to one, or `null`."
},
"escalation": {
"$ref": "#/components/schemas/AlertIncidentEscalation",
"description": "The incident's latest escalation chain run, or `null` when its severity tier names no chain."
},
"first_triggered_at": {
"type": "string"
},
Expand Down Expand Up @@ -523,6 +538,86 @@
"title": "Alert incident actor",
"type": "object"
},
"AlertIncidentEscalation": {
"additionalProperties": false,
"description": "How far the incident's latest escalation chain run has got.",
"properties": {
"chain_id": {
"$ref": "#/components/schemas/_maple_EscalationChainId"
},
"chain_name": {
"description": "The chain's name when the run started.",
"examples": [
"Checkout on-call"
],
"type": "string"
},
"next_step_at": {
"description": "When the next step pages, or `null` while paused or once the run has ended.",
"type": "string"
},
"pass": {
"description": "The pass the next step belongs to, from 1.",
"examples": [
1
],
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"pass_count": {
"description": "Passes over the chain in all: its repeat count plus one.",
"examples": [
2
],
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"status": {
"$ref": "#/components/schemas/_maple_AlertEscalationRunStatus"
},
"step": {
"description": "The next step to page, from 1.",
"examples": [
2
],
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"step_count": {
"description": "Steps in the chain the run started with.",
"examples": [
3
],
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
},
"steps_sent": {
"description": "Steps paged so far, across passes.",
"examples": [
1
],
"maximum": 9007199254740991,
"minimum": 0,
"type": "integer"
}
},
"required": [
"chain_id",
"chain_name",
"status",
"step",
"step_count",
"pass",
"pass_count",
"steps_sent"
],
"title": "Alert incident escalation",
"type": "object"
},
"AlertIncidentList": {
"additionalProperties": false,
"description": "A cursor-paginated page of alert incidents.",
Expand Down Expand Up @@ -6951,6 +7046,21 @@
"title": "Alert Destination Type",
"type": "string"
},
"_maple_AlertEscalationRunStatus": {
"description": "`active` waits on `next_step_at`; `acknowledged` is paused until the incident is unacknowledged; `resolved`, `exhausted` (every step and repeat sent) and `superseded` (a higher tier started its own chain) have ended.",
"enum": [
"active",
"acknowledged",
"resolved",
"exhausted",
"superseded"
],
"examples": [
"active"
],
"title": "Alert Escalation Run Status",
"type": "string"
},
"_maple_AlertEvaluationStatus": {
"description": "The evaluation verdict: `breached`, `healthy`, or `skipped` (not enough samples / no data).",
"enum": [
Expand Down
Loading
Loading